diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47fed3a154..dafe2ae844 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: check-yaml args: [ '--unsafe' ] @@ -21,8 +21,8 @@ repos: - id: requirements-txt-fixer - id: detect-private-key - id: fix-byte-order-marker - - repo: https://github.com/psf/black - rev: 23.11.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.12.0 hooks: - id: black language_version: python3 @@ -46,13 +46,13 @@ repos: # - id: mypy # files: ^(src/|tests/|plugins/) - repo: https://github.com/asottile/reorder-python-imports - rev: v3.12.0 + rev: v3.16.0 hooks: - id: reorder-python-imports args: [ --py37-plus, '--application-directories=.:src' ] # use latest python syntax - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [ --py37-plus ] @@ -65,7 +65,7 @@ repos: # args: [ -s, B101 ] # add copyright notice - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.4 + rev: v1.5.5 hooks: - id: insert-license files: \.java$ @@ -144,7 +144,7 @@ repos: - --license-filepath - NOTICE.txt - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + rev: v4.0.0-alpha.8 hooks: - id: prettier types_or: [scss, css, javascript, ts, html] diff --git a/examples/pgvector-embedder/40_ingest_embeddings.py b/examples/pgvector-embedder/40_ingest_embeddings.py index 289cb939c5..6ca5230043 100644 --- a/examples/pgvector-embedder/40_ingest_embeddings.py +++ b/examples/pgvector-embedder/40_ingest_embeddings.py @@ -41,9 +41,11 @@ def run(job_input: IJobInput): embedding_payload = { "id": composite_id, - "embedding": embeddings[i].tolist() - if isinstance(embeddings[i], np.ndarray) - else embeddings[i], + "embedding": ( + embeddings[i].tolist() + if isinstance(embeddings[i], np.ndarray) + else embeddings[i] + ), } job_input.send_object_for_ingestion( payload=embedding_payload, diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/data-jobs.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/data-jobs.spec.js index f5fbf51fe5..b6981555d8 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/data-jobs.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/data-jobs.spec.js @@ -5,10 +5,13 @@ /// -import { DataJobsExplorePage } from '../../../../support/pages/explore/data-jobs/data-jobs.po'; -import { DataJobExploreDetailsPage } from '../../../../support/pages/explore/data-jobs/details/data-job-details.po'; +import { DataJobsExplorePage } from "../../../../support/pages/explore/data-jobs/data-jobs.po"; +import { DataJobExploreDetailsPage } from "../../../../support/pages/explore/data-jobs/details/data-job-details.po"; -describe('Data Jobs Explore Page', { tags: ['@dataPipelines', '@exploreDataJobs', '@explore'] }, () => { +describe( + "Data Jobs Explore Page", + { tags: ["@dataPipelines", "@exploreDataJobs", "@explore"] }, + () => { /** * @type {DataJobsExplorePage} */ @@ -23,305 +26,464 @@ describe('Data Jobs Explore Page', { tags: ['@dataPipelines', '@exploreDataJobs' let additionalTestJobFixture; before(() => { - return DataJobsExplorePage.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('data-jobs-explore')) - .then(() => DataJobsExplorePage.login()) - .then(() => cy.saveLocalStorage('data-jobs-explore')) - .then(() => DataJobsExplorePage.deleteShortLivedTestJobsNoDeploy(true)) - .then(() => DataJobsExplorePage.createShortLivedTestJobsNoDeploy()) - .then(() => - DataJobsExplorePage.loadShortLivedTestJobsFixtureNoDeploy().then((fixtures) => { - testJobsFixture = [fixtures[0], fixtures[1]]; - additionalTestJobFixture = fixtures[2]; - - return cy.wrap({ - context: 'explore::data-jobs.spec::before()', - action: 'continue' - }); - }) - ); + return DataJobsExplorePage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("data-jobs-explore")) + .then(() => DataJobsExplorePage.login()) + .then(() => cy.saveLocalStorage("data-jobs-explore")) + .then(() => DataJobsExplorePage.deleteShortLivedTestJobsNoDeploy(true)) + .then(() => DataJobsExplorePage.createShortLivedTestJobsNoDeploy()) + .then(() => + DataJobsExplorePage.loadShortLivedTestJobsFixtureNoDeploy().then( + (fixtures) => { + testJobsFixture = [fixtures[0], fixtures[1]]; + additionalTestJobFixture = fixtures[2]; + + return cy.wrap({ + context: "explore::data-jobs.spec::before()", + action: "continue", + }); + }, + ), + ); }); after(() => { - DataJobsExplorePage.deleteShortLivedTestJobsNoDeploy(); + DataJobsExplorePage.deleteShortLivedTestJobsNoDeploy(); - DataJobsExplorePage.saveHarIfSupported(); + DataJobsExplorePage.saveHarIfSupported(); }); beforeEach(() => { - cy.restoreLocalStorage('data-jobs-explore'); + cy.restoreLocalStorage("data-jobs-explore"); - DataJobsExplorePage.wireUserSession(); - DataJobsExplorePage.initInterceptors(); + DataJobsExplorePage.wireUserSession(); + DataJobsExplorePage.initInterceptors(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it('page is loaded and shows data jobs', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateWithSideMenu(); + describe("smoke", { tags: ["@smoke"] }, () => { + it("page is loaded and shows data jobs", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateWithSideMenu(); - dataJobsExplorePage.getPageTitle().scrollIntoView().should('be.visible').invoke('text').invoke('trim').should('eq', 'Explore Data Jobs'); + dataJobsExplorePage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .invoke("text") + .invoke("trim") + .should("eq", "Explore Data Jobs"); - dataJobsExplorePage.getDataGrid().scrollIntoView().should('be.visible'); + dataJobsExplorePage.getDataGrid().scrollIntoView().should("be.visible"); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); - testJobsFixture.forEach((testJob) => { - cy.log('Fixture for name: ' + testJob.job_name); + testJobsFixture.forEach((testJob) => { + cy.log("Fixture for name: " + testJob.job_name); - dataJobsExplorePage.getDataGridCell(testJob.job_name).scrollIntoView().should('be.visible'); - }); + dataJobsExplorePage + .getDataGridCell(testJob.job_name) + .scrollIntoView() + .should("be.visible"); }); + }); - it('navigates to data job', () => { - cy.log('Fixture for name: ' + testJobsFixture[0].job_name); + it("navigates to data job", () => { + cy.log("Fixture for name: " + testJobsFixture[0].job_name); - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); - dataJobsExplorePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); + dataJobsExplorePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); - const dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); + const dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); - dataJobExploreDetailsPage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].job_name); + dataJobExploreDetailsPage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", testJobsFixture[0].job_name); - dataJobExploreDetailsPage.getDescriptionField().scrollIntoView().should('be.visible').should('contain.text', testJobsFixture[0].description); - }); + dataJobExploreDetailsPage + .getDescriptionField() + .scrollIntoView() + .should("be.visible") + .should("contain.text", testJobsFixture[0].description); + }); }); - describe('extended', () => { - it('refresh load newly created data jobs', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).invoke('text').invoke('trim').should('eq', testJobsFixture[0].job_name); - - dataJobsExplorePage.getDataGridCell(additionalTestJobFixture.job_name).should('not.exist'); - - DataJobsExplorePage.createAdditionalShortLivedTestJobsNoDeploy(); - - dataJobsExplorePage.refreshDataGrid(); - - dataJobsExplorePage.getDataGridCell(additionalTestJobFixture.job_name).invoke('text').invoke('trim').should('eq', additionalTestJobFixture.job_name); - }); - - it('filters data jobs', () => { - cy.log('Fixture for name: ' + testJobsFixture[0].job_name); - - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); - - it('searches data jobs', () => { - cy.log('Fixture for name: ' + testJobsFixture[0].job_name); - - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[1].job_name.substring(0, 20)); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // verify url contains jobName value - dataJobsExplorePage.getCurrentUrl().should('match', new RegExp(`\\/explore\\/data-jobs\\?jobName=${testJobsFixture[1].job_name.substring(0, 20)}&deploymentStatus=all$`)); - - dataJobsExplorePage.searchByJobName(testJobsFixture[0].job_name); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); - - it('searches data jobs, search parameter goes into URL', () => { - cy.log('Fixture for name: ' + testJobsFixture[0].job_name); - - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - // verify 2 test rows visible - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // do search - dataJobsExplorePage.searchByJobName(testJobsFixture[0].job_name); - - // verify 1 test row visible - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - - // verify url contains search value - dataJobsExplorePage.getCurrentUrl().should('match', new RegExp(`\\/explore\\/data-jobs\\?search=${testJobsFixture[0].job_name}&jobName=${testJobsFixture[0].job_name.substring(0, 20)}&deploymentStatus=all$`)); - - // clear search with clear() method - dataJobsExplorePage.clearSearchField(); - - // verify 2 test rows visible - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // verify url does not contain search value - dataJobsExplorePage.getCurrentUrl().should('match', new RegExp(`\\/explore\\/data-jobs\\?jobName=${testJobsFixture[0].job_name.substring(0, 20)}&deploymentStatus=all$`)); - }); - - it('searches data jobs, perform search when URL contains search parameter', () => { - cy.log('Fixture for name: ' + testJobsFixture[1].job_name); - - // navigate with search value in URL - dataJobsExplorePage = DataJobsExplorePage.navigateToDataJobUrl(`/explore/data-jobs?search=${testJobsFixture[1].job_name}`); - - dataJobsExplorePage.waitForGridToLoad(null); - - // verify url contains search value - dataJobsExplorePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/explore/data-jobs', - queryParams: { - search: testJobsFixture[1].job_name, - deploymentStatus: 'all' - } - }); - - // verify 1 test row visible - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).should('not.exist'); - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // clear search with button - dataJobsExplorePage.clearSearchFieldWithButton(); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - // verify 2 test rows visible - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // verify url does not contain search value - dataJobsExplorePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/explore/data-jobs', - queryParams: { - jobName: testJobsFixture[0].job_name.substring(0, 20), - deploymentStatus: 'all' - } - }); - }); - - it('filter description data jobs', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - // show panel for show/hide columns - dataJobsExplorePage.toggleColumnShowHidePanel(); - - // verify column is not checked in toggling menu - dataJobsExplorePage.getDataGridColumnShowHideOption('Description').should('exist').should('not.be.checked'); - - // verify header cell for column is not rendered - dataJobsExplorePage.getDataGridHeaderCell('Description').should('have.length', 0); - - // toggle column to render - dataJobsExplorePage.checkColumnShowHideOption('Description'); - - // verify column is checked in toggling menu - dataJobsExplorePage.getHeaderColumnDescriptionName().should('exist'); - - // filter by job description - dataJobsExplorePage.filterByJobDescription('Test description 1'); - - // verify url contains description value - dataJobsExplorePage.getCurrentUrl().should('match', new RegExp(`\\/explore\\/data-jobs\\?description=Test%20description%201&deploymentStatus=all$`)); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); - - it('perform filtering by description when URL contains description parameter', () => { - // navigate with description value in URL - dataJobsExplorePage = DataJobsExplorePage.navigateToDataJobUrl(`/explore/data-jobs?description=Test%20description%201`); - - dataJobsExplorePage.waitForGridToLoad(null); - - // show panel for show/hide columns - dataJobsExplorePage.toggleColumnShowHidePanel(); - - // toggle column to render - dataJobsExplorePage.checkColumnShowHideOption('Description'); - - // verify url contains description value - dataJobsExplorePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/explore/data-jobs', - queryParams: { - description: 'Test description 1', - deploymentStatus: 'all' - } - }); - - // verify 1 test row visible - dataJobsExplorePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - - dataJobsExplorePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); - - it('show/hide column when toggling from menu', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - // show panel for show/hide columns - dataJobsExplorePage.toggleColumnShowHidePanel(); - - // verify correct options are rendered - dataJobsExplorePage.getDataGridColumnShowHideOptionsValues().should('have.length', 10).invoke('join', ',').should('eq', 'Description,Deployment Status,Python Version,Last Execution Duration,Success rate,Next run (UTC),Last Deployed (UTC),Last Deployed By,Source,Logs'); - - // verify column is not checked in toggling menu - dataJobsExplorePage.getDataGridColumnShowHideOption('Last Execution Duration').should('exist').should('not.be.checked'); - - // verify header cell for column is not rendered - dataJobsExplorePage.getDataGridHeaderCell('Last Execution Duration').should('have.length', 0); - - // toggle column to render - dataJobsExplorePage.checkColumnShowHideOption('Last Execution Duration'); - - // verify column is checked in toggling menu - dataJobsExplorePage.getDataGridColumnShowHideOption('Last Execution Duration').should('exist').should('be.checked'); - - // verify header cell for column is rendered - dataJobsExplorePage.getDataGridHeaderCell('Last Execution Duration').should('have.length', 1); - - // toggle column to hide - dataJobsExplorePage.uncheckColumnShowHideOption('Last Execution Duration'); - - // verify column is not checked in toggling menu - dataJobsExplorePage.getDataGridColumnShowHideOption('Last Execution Duration').should('exist').should('not.be.checked'); - - // verify header cell for column is not rendered - dataJobsExplorePage.getDataGridHeaderCell('Last Execution Duration').should('have.length', 0); - - // hide panel for show/hide columns - dataJobsExplorePage.toggleColumnShowHidePanel(); - }); + describe("extended", () => { + it("refresh load newly created data jobs", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .invoke("text") + .invoke("trim") + .should("eq", testJobsFixture[0].job_name); + + dataJobsExplorePage + .getDataGridCell(additionalTestJobFixture.job_name) + .should("not.exist"); + + DataJobsExplorePage.createAdditionalShortLivedTestJobsNoDeploy(); + + dataJobsExplorePage.refreshDataGrid(); + + dataJobsExplorePage + .getDataGridCell(additionalTestJobFixture.job_name) + .invoke("text") + .invoke("trim") + .should("eq", additionalTestJobFixture.job_name); + }); + + it("filters data jobs", () => { + cy.log("Fixture for name: " + testJobsFixture[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); + + it("searches data jobs", () => { + cy.log("Fixture for name: " + testJobsFixture[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[1].job_name.substring(0, 20), + ); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // verify url contains jobName value + dataJobsExplorePage + .getCurrentUrl() + .should( + "match", + new RegExp( + `\\/explore\\/data-jobs\\?jobName=${testJobsFixture[1].job_name.substring(0, 20)}&deploymentStatus=all$`, + ), + ); + + dataJobsExplorePage.searchByJobName(testJobsFixture[0].job_name); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); + + it("searches data jobs, search parameter goes into URL", () => { + cy.log("Fixture for name: " + testJobsFixture[0].job_name); + + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + // verify 2 test rows visible + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // do search + dataJobsExplorePage.searchByJobName(testJobsFixture[0].job_name); + + // verify 1 test row visible + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + + // verify url contains search value + dataJobsExplorePage + .getCurrentUrl() + .should( + "match", + new RegExp( + `\\/explore\\/data-jobs\\?search=${testJobsFixture[0].job_name}&jobName=${testJobsFixture[0].job_name.substring(0, 20)}&deploymentStatus=all$`, + ), + ); + + // clear search with clear() method + dataJobsExplorePage.clearSearchField(); + + // verify 2 test rows visible + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // verify url does not contain search value + dataJobsExplorePage + .getCurrentUrl() + .should( + "match", + new RegExp( + `\\/explore\\/data-jobs\\?jobName=${testJobsFixture[0].job_name.substring(0, 20)}&deploymentStatus=all$`, + ), + ); + }); + + it("searches data jobs, perform search when URL contains search parameter", () => { + cy.log("Fixture for name: " + testJobsFixture[1].job_name); + + // navigate with search value in URL + dataJobsExplorePage = DataJobsExplorePage.navigateToDataJobUrl( + `/explore/data-jobs?search=${testJobsFixture[1].job_name}`, + ); + + dataJobsExplorePage.waitForGridToLoad(null); + + // verify url contains search value + dataJobsExplorePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/explore/data-jobs", + queryParams: { + search: testJobsFixture[1].job_name, + deploymentStatus: "all", + }, + }); + + // verify 1 test row visible + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .should("not.exist"); + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // clear search with button + dataJobsExplorePage.clearSearchFieldWithButton(); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + // verify 2 test rows visible + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // verify url does not contain search value + dataJobsExplorePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/explore/data-jobs", + queryParams: { + jobName: testJobsFixture[0].job_name.substring(0, 20), + deploymentStatus: "all", + }, + }); + }); + + it("filter description data jobs", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // show panel for show/hide columns + dataJobsExplorePage.toggleColumnShowHidePanel(); + + // verify column is not checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption("Description") + .should("exist") + .should("not.be.checked"); + + // verify header cell for column is not rendered + dataJobsExplorePage + .getDataGridHeaderCell("Description") + .should("have.length", 0); + + // toggle column to render + dataJobsExplorePage.checkColumnShowHideOption("Description"); + + // verify column is checked in toggling menu + dataJobsExplorePage.getHeaderColumnDescriptionName().should("exist"); + + // filter by job description + dataJobsExplorePage.filterByJobDescription("Test description 1"); + + // verify url contains description value + dataJobsExplorePage + .getCurrentUrl() + .should( + "match", + new RegExp( + `\\/explore\\/data-jobs\\?description=Test%20description%201&deploymentStatus=all$`, + ), + ); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); + + it("perform filtering by description when URL contains description parameter", () => { + // navigate with description value in URL + dataJobsExplorePage = DataJobsExplorePage.navigateToDataJobUrl( + `/explore/data-jobs?description=Test%20description%201`, + ); + + dataJobsExplorePage.waitForGridToLoad(null); + + // show panel for show/hide columns + dataJobsExplorePage.toggleColumnShowHidePanel(); + + // toggle column to render + dataJobsExplorePage.checkColumnShowHideOption("Description"); + + // verify url contains description value + dataJobsExplorePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/explore/data-jobs", + queryParams: { + description: "Test description 1", + deploymentStatus: "all", + }, + }); + + // verify 1 test row visible + dataJobsExplorePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + + dataJobsExplorePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); + + it("show/hide column when toggling from menu", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // show panel for show/hide columns + dataJobsExplorePage.toggleColumnShowHidePanel(); + + // verify correct options are rendered + dataJobsExplorePage + .getDataGridColumnShowHideOptionsValues() + .should("have.length", 10) + .invoke("join", ",") + .should( + "eq", + "Description,Deployment Status,Python Version,Last Execution Duration,Success rate,Next run (UTC),Last Deployed (UTC),Last Deployed By,Source,Logs", + ); + + // verify column is not checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption("Last Execution Duration") + .should("exist") + .should("not.be.checked"); + + // verify header cell for column is not rendered + dataJobsExplorePage + .getDataGridHeaderCell("Last Execution Duration") + .should("have.length", 0); + + // toggle column to render + dataJobsExplorePage.checkColumnShowHideOption( + "Last Execution Duration", + ); + + // verify column is checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption("Last Execution Duration") + .should("exist") + .should("be.checked"); + + // verify header cell for column is rendered + dataJobsExplorePage + .getDataGridHeaderCell("Last Execution Duration") + .should("have.length", 1); + + // toggle column to hide + dataJobsExplorePage.uncheckColumnShowHideOption( + "Last Execution Duration", + ); + + // verify column is not checked in toggling menu + dataJobsExplorePage + .getDataGridColumnShowHideOption("Last Execution Duration") + .should("exist") + .should("not.be.checked"); + + // verify header cell for column is not rendered + dataJobsExplorePage + .getDataGridHeaderCell("Last Execution Duration") + .should("have.length", 0); + + // hide panel for show/hide columns + dataJobsExplorePage.toggleColumnShowHidePanel(); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/details/data-job-details.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/details/data-job-details.spec.js index 90d867f2b6..c5685bfb10 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/details/data-job-details.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/details/data-job-details.spec.js @@ -5,10 +5,13 @@ /// -import { DataJobsExplorePage } from '../../../../../support/pages/explore/data-jobs/data-jobs.po'; -import { DataJobExploreDetailsPage } from '../../../../../support/pages/explore/data-jobs/details/data-job-details.po'; +import { DataJobsExplorePage } from "../../../../../support/pages/explore/data-jobs/data-jobs.po"; +import { DataJobExploreDetailsPage } from "../../../../../support/pages/explore/data-jobs/details/data-job-details.po"; -describe('Data Job Explore Details Page', { tags: ['@dataPipelines', '@exploreDataJobDetails', '@explore'] }, () => { +describe( + "Data Job Explore Details Page", + { tags: ["@dataPipelines", "@exploreDataJobDetails", "@explore"] }, + () => { /** * @type {DataJobExploreDetailsPage} */ @@ -23,101 +26,191 @@ describe('Data Job Explore Details Page', { tags: ['@dataPipelines', '@exploreDa let dataJobsExplorePage; before(() => { - return DataJobExploreDetailsPage.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('data-job-explore-details')) - .then(() => DataJobExploreDetailsPage.login()) - .then(() => cy.saveLocalStorage('data-job-explore-details')) - .then(() => DataJobExploreDetailsPage.deleteShortLivedTestJobsNoDeploy(true)) - .then(() => DataJobExploreDetailsPage.createShortLivedTestJobsNoDeploy()) - .then(() => - DataJobExploreDetailsPage.loadShortLivedTestJobsFixtureNoDeploy().then((fixtures) => { - testJobsFixture = [fixtures[0], fixtures[1]]; - - return cy.wrap({ - context: 'explore::data-job-details.spec::before()', - action: 'continue' - }); - }) - ); + return DataJobExploreDetailsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("data-job-explore-details")) + .then(() => DataJobExploreDetailsPage.login()) + .then(() => cy.saveLocalStorage("data-job-explore-details")) + .then(() => + DataJobExploreDetailsPage.deleteShortLivedTestJobsNoDeploy(true), + ) + .then(() => + DataJobExploreDetailsPage.createShortLivedTestJobsNoDeploy(), + ) + .then(() => + DataJobExploreDetailsPage.loadShortLivedTestJobsFixtureNoDeploy().then( + (fixtures) => { + testJobsFixture = [fixtures[0], fixtures[1]]; + + return cy.wrap({ + context: "explore::data-job-details.spec::before()", + action: "continue", + }); + }, + ), + ); }); after(() => { - DataJobExploreDetailsPage.deleteShortLivedTestJobsNoDeploy(); + DataJobExploreDetailsPage.deleteShortLivedTestJobsNoDeploy(); - DataJobExploreDetailsPage.saveHarIfSupported(); + DataJobExploreDetailsPage.saveHarIfSupported(); }); beforeEach(() => { - cy.restoreLocalStorage('data-job-explore-details'); + cy.restoreLocalStorage("data-job-explore-details"); - DataJobExploreDetailsPage.wireUserSession(); - DataJobExploreDetailsPage.initInterceptors(); + DataJobExploreDetailsPage.wireUserSession(); + DataJobExploreDetailsPage.initInterceptors(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it('should load and show job details', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateWithSideMenu(); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - dataJobsExplorePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); - - dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); - - dataJobExploreDetailsPage.getDetailsTab().scrollIntoView().should('be.visible'); - - dataJobExploreDetailsPage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].job_name); - - dataJobExploreDetailsPage.getStatusField().scrollIntoView().should('be.visible').should('have.text', 'Not Deployed'); - - dataJobExploreDetailsPage.getDescriptionField().scrollIntoView().should('be.visible').should('contain.text', testJobsFixture[0].description); - - dataJobExploreDetailsPage.getTeamField().scrollIntoView().should('be.visible').should('have.text', testJobsFixture[0].team); - - dataJobExploreDetailsPage.getScheduleField().scrollIntoView().should('be.visible').should('contains.text', 'At 12:00 AM, on day 01 of the month, and on Friday'); - - dataJobExploreDetailsPage.getOnDeployedField().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].config.contacts.notified_on_job_deploy); - - dataJobExploreDetailsPage.getOnPlatformErrorField().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].config.contacts.notified_on_job_failure_platform_error); - - dataJobExploreDetailsPage.getOnUserErrorField().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].config.contacts.notified_on_job_failure_user_error); - - dataJobExploreDetailsPage.getOnSuccessField().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].config.contacts.notified_on_job_success); - }); - - it('should verify Details tab is visible and active', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - dataJobsExplorePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); - - const dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); - - dataJobExploreDetailsPage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].job_name); - - dataJobExploreDetailsPage.getDetailsTab().scrollIntoView().should('be.visible').should('have.class', 'active'); - }); + describe("smoke", { tags: ["@smoke"] }, () => { + it("should load and show job details", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateWithSideMenu(); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + dataJobsExplorePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); + + dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); + + dataJobExploreDetailsPage + .getDetailsTab() + .scrollIntoView() + .should("be.visible"); + + dataJobExploreDetailsPage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", testJobsFixture[0].job_name); + + dataJobExploreDetailsPage + .getStatusField() + .scrollIntoView() + .should("be.visible") + .should("have.text", "Not Deployed"); + + dataJobExploreDetailsPage + .getDescriptionField() + .scrollIntoView() + .should("be.visible") + .should("contain.text", testJobsFixture[0].description); + + dataJobExploreDetailsPage + .getTeamField() + .scrollIntoView() + .should("be.visible") + .should("have.text", testJobsFixture[0].team); + + dataJobExploreDetailsPage + .getScheduleField() + .scrollIntoView() + .should("be.visible") + .should( + "contains.text", + "At 12:00 AM, on day 01 of the month, and on Friday", + ); + + dataJobExploreDetailsPage + .getOnDeployedField() + .scrollIntoView() + .should("be.visible") + .should( + "contains.text", + testJobsFixture[0].config.contacts.notified_on_job_deploy, + ); + + dataJobExploreDetailsPage + .getOnPlatformErrorField() + .scrollIntoView() + .should("be.visible") + .should( + "contains.text", + testJobsFixture[0].config.contacts + .notified_on_job_failure_platform_error, + ); + + dataJobExploreDetailsPage + .getOnUserErrorField() + .scrollIntoView() + .should("be.visible") + .should( + "contains.text", + testJobsFixture[0].config.contacts + .notified_on_job_failure_user_error, + ); + + dataJobExploreDetailsPage + .getOnSuccessField() + .scrollIntoView() + .should("be.visible") + .should( + "contains.text", + testJobsFixture[0].config.contacts.notified_on_job_success, + ); + }); + + it("should verify Details tab is visible and active", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + dataJobsExplorePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); + + const dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); + + dataJobExploreDetailsPage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", testJobsFixture[0].job_name); + + dataJobExploreDetailsPage + .getDetailsTab() + .scrollIntoView() + .should("be.visible") + .should("have.class", "active"); + }); }); - describe('extended', () => { - it('should verify Action buttons are not displayed', () => { - dataJobsExplorePage = DataJobsExplorePage.navigateTo(); + describe("extended", () => { + it("should verify Action buttons are not displayed", () => { + dataJobsExplorePage = DataJobsExplorePage.navigateTo(); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); - dataJobsExplorePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); + dataJobsExplorePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); - const dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); + const dataJobExploreDetailsPage = DataJobExploreDetailsPage.getPage(); - dataJobExploreDetailsPage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].job_name); + dataJobExploreDetailsPage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", testJobsFixture[0].job_name); - dataJobExploreDetailsPage.getExecuteNowButton().should('not.exist'); + dataJobExploreDetailsPage.getExecuteNowButton().should("not.exist"); - dataJobExploreDetailsPage.getActionDropdownBtn().should('not.exist'); - }); + dataJobExploreDetailsPage.getActionDropdownBtn().should("not.exist"); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/executions/data-job-executions.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/executions/data-job-executions.spec.js index 1e3e3b0cd9..296712c474 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/executions/data-job-executions.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/explore/data-jobs/executions/data-job-executions.spec.js @@ -5,93 +5,119 @@ /// -import { DataJobDetailsBasePO } from '../../../../../support/pages/base/data-pipelines/data-job-details-base.po'; +import { DataJobDetailsBasePO } from "../../../../../support/pages/base/data-pipelines/data-job-details-base.po"; -import { DataJobsExplorePage } from '../../../../../support/pages/explore/data-jobs/data-jobs.po'; -import { DataJobExploreExecutionsPage } from '../../../../../support/pages/explore/data-jobs/executions/data-job-executions.po'; +import { DataJobsExplorePage } from "../../../../../support/pages/explore/data-jobs/data-jobs.po"; +import { DataJobExploreExecutionsPage } from "../../../../../support/pages/explore/data-jobs/executions/data-job-executions.po"; -describe('Data Job Explore Executions Page', { tags: ['@dataPipelines', '@exploreDataJobExecutions', '@explore'] }, () => { +describe( + "Data Job Explore Executions Page", + { tags: ["@dataPipelines", "@exploreDataJobExecutions", "@explore"] }, + () => { /** * @type {Array<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} */ let testJobsFixture; before(() => { - return DataJobExploreExecutionsPage.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('data-job-explore-executions')) - .then(() => DataJobExploreExecutionsPage.login()) - .then(() => cy.saveLocalStorage('data-job-explore-executions')) - .then(() => DataJobExploreExecutionsPage.deleteShortLivedTestJobsNoDeploy(true)) - .then(() => DataJobExploreExecutionsPage.createShortLivedTestJobsNoDeploy()) - .then(() => - DataJobExploreExecutionsPage.loadShortLivedTestJobsFixtureNoDeploy().then((fixtures) => { - testJobsFixture = [fixtures[0], fixtures[1]]; - - return cy.wrap({ - context: 'explore::data-job-executions.spec::before()', - action: 'continue' - }); - }) - ); + return DataJobExploreExecutionsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("data-job-explore-executions")) + .then(() => DataJobExploreExecutionsPage.login()) + .then(() => cy.saveLocalStorage("data-job-explore-executions")) + .then(() => + DataJobExploreExecutionsPage.deleteShortLivedTestJobsNoDeploy(true), + ) + .then(() => + DataJobExploreExecutionsPage.createShortLivedTestJobsNoDeploy(), + ) + .then(() => + DataJobExploreExecutionsPage.loadShortLivedTestJobsFixtureNoDeploy().then( + (fixtures) => { + testJobsFixture = [fixtures[0], fixtures[1]]; + + return cy.wrap({ + context: "explore::data-job-executions.spec::before()", + action: "continue", + }); + }, + ), + ); }); after(() => { - DataJobExploreExecutionsPage.deleteShortLivedTestJobsNoDeploy(); + DataJobExploreExecutionsPage.deleteShortLivedTestJobsNoDeploy(); - DataJobExploreExecutionsPage.saveHarIfSupported(); + DataJobExploreExecutionsPage.saveHarIfSupported(); }); beforeEach(() => { - cy.restoreLocalStorage('data-job-explore-executions'); + cy.restoreLocalStorage("data-job-explore-executions"); - DataJobExploreExecutionsPage.wireUserSession(); - DataJobExploreExecutionsPage.initInterceptors(); + DataJobExploreExecutionsPage.wireUserSession(); + DataJobExploreExecutionsPage.initInterceptors(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it(`should open Details and verify Executions tab is not displayed`, () => { - cy.log('Fixture for name: ' + testJobsFixture[0].job_name); + describe("smoke", { tags: ["@smoke"] }, () => { + it(`should open Details and verify Executions tab is not displayed`, () => { + cy.log("Fixture for name: " + testJobsFixture[0].job_name); - /** - * @type {DataJobsExplorePage} - */ - const dataJobsExplorePage = DataJobsExplorePage.navigateWithSideMenu(); + /** + * @type {DataJobsExplorePage} + */ + const dataJobsExplorePage = DataJobsExplorePage.navigateWithSideMenu(); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsExplorePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsExplorePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); - dataJobsExplorePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); + dataJobsExplorePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); - const dataJobDetailsBasePage = DataJobDetailsBasePO.getPage(); + const dataJobDetailsBasePage = DataJobDetailsBasePO.getPage(); - dataJobDetailsBasePage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].job_name); + dataJobDetailsBasePage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", testJobsFixture[0].job_name); - dataJobDetailsBasePage.getDetailsTab().should('have.class', 'active'); + dataJobDetailsBasePage.getDetailsTab().should("have.class", "active"); - dataJobDetailsBasePage.getExecutionsTab().should('not.exist'); - }); + dataJobDetailsBasePage.getExecutionsTab().should("not.exist"); + }); }); - describe('extended', () => { - it('should verify on URL navigate to Executions will redirect to Details', () => { - /** - * @type {DataJobExploreExecutionsPage} - */ - const dataJobBasePage = DataJobExploreExecutionsPage.navigateTo(testJobsFixture[0].team, testJobsFixture[0].job_name); - - dataJobBasePage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', testJobsFixture[0].job_name); - - dataJobBasePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/explore/data-jobs/${testJobsFixture[0].team}/${testJobsFixture[0].job_name}/details`, - queryParams: {} - }); - - dataJobBasePage.getDetailsTab().should('have.class', 'active'); - }); + describe("extended", () => { + it("should verify on URL navigate to Executions will redirect to Details", () => { + /** + * @type {DataJobExploreExecutionsPage} + */ + const dataJobBasePage = DataJobExploreExecutionsPage.navigateTo( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); + + dataJobBasePage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", testJobsFixture[0].job_name); + + dataJobBasePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/explore/data-jobs/${testJobsFixture[0].team}/${testJobsFixture[0].job_name}/details`, + queryParams: {}, + }); + + dataJobBasePage.getDetailsTab().should("have.class", "active"); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page-data-jobs-health-overview.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page-data-jobs-health-overview.spec.js index 2d5fd19926..b4bca65971 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page-data-jobs-health-overview.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page-data-jobs-health-overview.spec.js @@ -5,13 +5,16 @@ /// -import { TEAM_VDK_DATA_JOB_FAILING } from '../../../support/helpers/constants.support'; +import { TEAM_VDK_DATA_JOB_FAILING } from "../../../support/helpers/constants.support"; -import { GetStartedDataJobsHealthOverviewWidgetPO } from '../../../support/pages/get-started/get-started-page-data-jobs-health-overview.po'; -import { DataJobManageDetailsPage } from '../../../support/pages/manage/data-jobs/details/data-job-details.po'; -import { DataJobManageExecutionsPage } from '../../../support/pages/manage/data-jobs/executions/data-job-executions.po'; +import { GetStartedDataJobsHealthOverviewWidgetPO } from "../../../support/pages/get-started/get-started-page-data-jobs-health-overview.po"; +import { DataJobManageDetailsPage } from "../../../support/pages/manage/data-jobs/details/data-job-details.po"; +import { DataJobManageExecutionsPage } from "../../../support/pages/manage/data-jobs/executions/data-job-executions.po"; -describe('Get Started Page: Data Jobs Health Overview Widget', { tags: ['@dataPipelines', '@healthOverviewWidget', '@getStarted'] }, () => { +describe( + "Get Started Page: Data Jobs Health Overview Widget", + { tags: ["@dataPipelines", "@healthOverviewWidget", "@getStarted"] }, + () => { /** * @type {{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}} */ @@ -22,101 +25,167 @@ describe('Get Started Page: Data Jobs Health Overview Widget', { tags: ['@dataPi let getStartedPage; before(() => { - return GetStartedDataJobsHealthOverviewWidgetPO.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('get-started-page-data-jobs-health')) - .then(() => GetStartedDataJobsHealthOverviewWidgetPO.login()) - .then(() => cy.saveLocalStorage('get-started-page-data-jobs-health')) - .then(() => GetStartedDataJobsHealthOverviewWidgetPO.createLongLivedJobs('failing')) - .then(() => GetStartedDataJobsHealthOverviewWidgetPO.provideExecutionsForLongLivedJobs({ job: 'failing' })) - .then(() => - GetStartedDataJobsHealthOverviewWidgetPO.loadLongLivedFailingJobFixture().then((loadedTestJob) => { - longLivedFailingJobFixture = loadedTestJob; - - return cy.wrap({ - context: 'get-started::1::get-started-page-data-jobs-health-overview.spec::before()', - action: 'continue' - }); - }) - ) - .then(() => { - return cy.wrap({ - context: 'get-started::2::get-started-page-data-jobs-health-overview.spec::before()', - action: 'continue' - }); - }); + return GetStartedDataJobsHealthOverviewWidgetPO.recordHarIfSupported() + .then(() => + cy.clearLocalStorageSnapshot("get-started-page-data-jobs-health"), + ) + .then(() => GetStartedDataJobsHealthOverviewWidgetPO.login()) + .then(() => cy.saveLocalStorage("get-started-page-data-jobs-health")) + .then(() => + GetStartedDataJobsHealthOverviewWidgetPO.createLongLivedJobs( + "failing", + ), + ) + .then(() => + GetStartedDataJobsHealthOverviewWidgetPO.provideExecutionsForLongLivedJobs( + { job: "failing" }, + ), + ) + .then(() => + GetStartedDataJobsHealthOverviewWidgetPO.loadLongLivedFailingJobFixture().then( + (loadedTestJob) => { + longLivedFailingJobFixture = loadedTestJob; + + return cy.wrap({ + context: + "get-started::1::get-started-page-data-jobs-health-overview.spec::before()", + action: "continue", + }); + }, + ), + ) + .then(() => { + return cy.wrap({ + context: + "get-started::2::get-started-page-data-jobs-health-overview.spec::before()", + action: "continue", + }); + }); }); after(() => { - return GetStartedDataJobsHealthOverviewWidgetPO.saveHarIfSupported().then(() => - cy.wrap({ - context: 'get-started::get-started-page-data-jobs-health-overview.spec::after()', - action: 'continue' - }) - ); + return GetStartedDataJobsHealthOverviewWidgetPO.saveHarIfSupported().then( + () => + cy.wrap({ + context: + "get-started::get-started-page-data-jobs-health-overview.spec::after()", + action: "continue", + }), + ); }); beforeEach(() => { - cy.restoreLocalStorage('get-started-page-data-jobs-health'); + cy.restoreLocalStorage("get-started-page-data-jobs-health"); - GetStartedDataJobsHealthOverviewWidgetPO.wireUserSession(); - GetStartedDataJobsHealthOverviewWidgetPO.initInterceptors(); + GetStartedDataJobsHealthOverviewWidgetPO.wireUserSession(); + GetStartedDataJobsHealthOverviewWidgetPO.initInterceptors(); - getStartedPage = GetStartedDataJobsHealthOverviewWidgetPO.navigateTo(); + getStartedPage = GetStartedDataJobsHealthOverviewWidgetPO.navigateTo(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it('Validate Data Jobs Health Overview panel', () => { - getStartedPage.getPageTitle().should('contain.text', 'Get Started with Data Pipelines'); - - getStartedPage.getDataJobsHealthPanel().scrollIntoView(); - - getStartedPage.getNumberOfFailedExecutions().should('be.gt', 0); - getStartedPage - .getAllFailingJobs() - .then((elements) => elements.filter((_index, el) => el.innerText.includes(TEAM_VDK_DATA_JOB_FAILING))) - .should('have.length.gt', 0); - getStartedPage - .getAllMostRecentFailingJobs() - .then((elements) => elements.filter((_index, el) => el.innerText.includes(TEAM_VDK_DATA_JOB_FAILING))) - .should('have.length.gt', 0); - }); + describe("smoke", { tags: ["@smoke"] }, () => { + it("Validate Data Jobs Health Overview panel", () => { + getStartedPage + .getPageTitle() + .should("contain.text", "Get Started with Data Pipelines"); + + getStartedPage.getDataJobsHealthPanel().scrollIntoView(); + + getStartedPage.getNumberOfFailedExecutions().should("be.gt", 0); + getStartedPage + .getAllFailingJobs() + .then((elements) => + elements.filter((_index, el) => + el.innerText.includes(TEAM_VDK_DATA_JOB_FAILING), + ), + ) + .should("have.length.gt", 0); + getStartedPage + .getAllMostRecentFailingJobs() + .then((elements) => + elements.filter((_index, el) => + el.innerText.includes(TEAM_VDK_DATA_JOB_FAILING), + ), + ) + .should("have.length.gt", 0); + }); }); - describe('extended', () => { - it('Verify Widgets rendered correct data and failing jobs navigates correctly', () => { - getStartedPage.getDataJobsHealthPanel().scrollIntoView(); - - getStartedPage.getExecutionsSuccessPercentage().should('be.gte', 0).should('be.lte', 100); - getStartedPage.getNumberOfFailedExecutions().should('be.gte', 2); - getStartedPage.getExecutionsTotal().should('be.gte', 2); - - getStartedPage.getAllFailingJobs().should('have.length.gte', 1); + describe("extended", () => { + it("Verify Widgets rendered correct data and failing jobs navigates correctly", () => { + getStartedPage.getDataJobsHealthPanel().scrollIntoView(); - getStartedPage.getAllMostRecentFailingJobsLinks().should('have.length.gte', 1); + getStartedPage + .getExecutionsSuccessPercentage() + .should("be.gte", 0) + .should("be.lte", 100); + getStartedPage.getNumberOfFailedExecutions().should("be.gte", 2); + getStartedPage.getExecutionsTotal().should("be.gte", 2); - // navigate to failing job details - getStartedPage.navigateToFailingJobDetails(longLivedFailingJobFixture.job_name); + getStartedPage.getAllFailingJobs().should("have.length.gte", 1); - const dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); - dataJobManageDetailsPage.getPageTitle().should('contain.text', `Data Job: ${longLivedFailingJobFixture.job_name}`); - dataJobManageDetailsPage.getDetailsTab().should('be.visible').should('have.class', 'active'); - dataJobManageDetailsPage.getExecutionsTab().should('exist').should('not.have.class', 'active'); - dataJobManageDetailsPage.showMoreDescription().getDescriptionFull().should('contain.text', longLivedFailingJobFixture.description); - }); - - it('Verify most recent failing executions Widget navigates correctly', () => { - getStartedPage.getDataJobsHealthPanel().scrollIntoView(); + getStartedPage + .getAllMostRecentFailingJobsLinks() + .should("have.length.gte", 1); - getStartedPage.getAllMostRecentFailingJobsLinks().should('have.length.gte', 1); + // navigate to failing job details + getStartedPage.navigateToFailingJobDetails( + longLivedFailingJobFixture.job_name, + ); - // navigate to most recent failing job executions - getStartedPage.navigateToMostRecentFailingJobExecutions(longLivedFailingJobFixture.job_name); + const dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); + dataJobManageDetailsPage + .getPageTitle() + .should( + "contain.text", + `Data Job: ${longLivedFailingJobFixture.job_name}`, + ); + dataJobManageDetailsPage + .getDetailsTab() + .should("be.visible") + .should("have.class", "active"); + dataJobManageDetailsPage + .getExecutionsTab() + .should("exist") + .should("not.have.class", "active"); + dataJobManageDetailsPage + .showMoreDescription() + .getDescriptionFull() + .should("contain.text", longLivedFailingJobFixture.description); + }); + + it("Verify most recent failing executions Widget navigates correctly", () => { + getStartedPage.getDataJobsHealthPanel().scrollIntoView(); + + getStartedPage + .getAllMostRecentFailingJobsLinks() + .should("have.length.gte", 1); + + // navigate to most recent failing job executions + getStartedPage.navigateToMostRecentFailingJobExecutions( + longLivedFailingJobFixture.job_name, + ); - const dataJobManageExecutionsPage = DataJobManageExecutionsPage.getPage(); - dataJobManageExecutionsPage.getPageTitle().should('contain.text', `Data Job: ${longLivedFailingJobFixture.job_name}`); - dataJobManageExecutionsPage.getDetailsTab().should('be.visible').should('not.have.class', 'active'); - dataJobManageExecutionsPage.getExecutionsTab().should('be.visible').should('have.class', 'active'); - dataJobManageExecutionsPage.getDataGridRows().should('have.length.gte', 2); - }); + const dataJobManageExecutionsPage = + DataJobManageExecutionsPage.getPage(); + dataJobManageExecutionsPage + .getPageTitle() + .should( + "contain.text", + `Data Job: ${longLivedFailingJobFixture.job_name}`, + ); + dataJobManageExecutionsPage + .getDetailsTab() + .should("be.visible") + .should("not.have.class", "active"); + dataJobManageExecutionsPage + .getExecutionsTab() + .should("be.visible") + .should("have.class", "active"); + dataJobManageExecutionsPage + .getDataGridRows() + .should("have.length.gte", 2); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page.spec.js index be191f2e14..62f5a1b8a3 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/get-started/get-started-page.spec.js @@ -5,46 +5,54 @@ /// -import { GetStartedPagePO } from '../../../support/pages/get-started/get-started-page.po'; +import { GetStartedPagePO } from "../../../support/pages/get-started/get-started-page.po"; -describe('Get Started Page', { tags: ['@dataPipelines', '@getStarted'] }, () => { +describe( + "Get Started Page", + { tags: ["@dataPipelines", "@getStarted"] }, + () => { /** * @type {GetStartedPagePO} */ let getStartedPage; before(() => { - return GetStartedPagePO.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('get-started-page')) - .then(() => cy.saveLocalStorage('get-started-page')) - .then(() => { - return cy.wrap({ - context: 'get-started::1::get-started-page.spec::before()', - action: 'continue' - }); - }); + return GetStartedPagePO.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("get-started-page")) + .then(() => cy.saveLocalStorage("get-started-page")) + .then(() => { + return cy.wrap({ + context: "get-started::1::get-started-page.spec::before()", + action: "continue", + }); + }); }); after(() => { - return GetStartedPagePO.saveHarIfSupported().then(() => - cy.wrap({ - context: 'get-started::get-started-page.spec::after()', - action: 'continue' - }) - ); + return GetStartedPagePO.saveHarIfSupported().then(() => + cy.wrap({ + context: "get-started::get-started-page.spec::after()", + action: "continue", + }), + ); }); beforeEach(() => { - cy.restoreLocalStorage('get-started-page'); + cy.restoreLocalStorage("get-started-page"); - GetStartedPagePO.wireUserSession(); - GetStartedPagePO.initInterceptors(); - getStartedPage = GetStartedPagePO.navigateTo(); + GetStartedPagePO.wireUserSession(); + GetStartedPagePO.initInterceptors(); + getStartedPage = GetStartedPagePO.navigateTo(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it('Main Title Component have text: Get Started with Data Pipelines', () => { - getStartedPage.getPageTitle().invoke('text').invoke('trim').should('eq', 'Get Started with Data Pipelines'); - }); + describe("smoke", { tags: ["@smoke"] }, () => { + it("Main Title Component have text: Get Started with Data Pipelines", () => { + getStartedPage + .getPageTitle() + .invoke("text") + .invoke("trim") + .should("eq", "Get Started with Data Pipelines"); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/data-jobs.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/data-jobs.spec.js index 61ebbeb02a..374515f4af 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/data-jobs.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/data-jobs.spec.js @@ -5,10 +5,13 @@ /// -import { DataJobsManagePage } from '../../../../support/pages/manage/data-jobs/data-jobs.po'; -import { DataJobManageDetailsPage } from '../../../../support/pages/manage/data-jobs/details/data-job-details.po'; +import { DataJobsManagePage } from "../../../../support/pages/manage/data-jobs/data-jobs.po"; +import { DataJobManageDetailsPage } from "../../../../support/pages/manage/data-jobs/details/data-job-details.po"; -describe('Data Jobs Manage Page', { tags: ['@dataPipelines', '@manageDataJobs', '@manage'] }, () => { +describe( + "Data Jobs Manage Page", + { tags: ["@dataPipelines", "@manageDataJobs", "@manage"] }, + () => { const descriptionWordsBeforeTruncate = 12; /** @@ -29,445 +32,628 @@ describe('Data Jobs Manage Page', { tags: ['@dataPipelines', '@manageDataJobs', let shortLivedTestJobWithDeployFixture; before(() => { - return DataJobsManagePage.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('data-jobs-manage')) - .then(() => DataJobsManagePage.login()) - .then(() => cy.saveLocalStorage('data-jobs-manage')) - .then(() => DataJobsManagePage.deleteShortLivedTestJobsNoDeploy(true)) - .then(() => DataJobsManagePage.createLongLivedJobs('failing')) - .then(() => DataJobsManagePage.createShortLivedTestJobWithDeploy('v0')) - .then(() => - DataJobsManagePage.loadShortLivedTestJobFixtureWithDeploy('v0').then((loadedTestJob) => { - shortLivedTestJobWithDeployFixture = loadedTestJob; - - return cy.wrap({ - context: 'manage::1::data-jobs.int.spec::before()', - action: 'continue' - }); - }) - ) - .then(() => DataJobsManagePage.createShortLivedTestJobsNoDeploy()) - .then(() => - DataJobsManagePage.loadShortLivedTestJobsFixtureNoDeploy().then((fixtures) => { - testJobsFixture = [fixtures[0], fixtures[1]]; - additionalTestJobFixture = fixtures[2]; - - return cy.wrap({ - context: 'manage::2::data-jobs.int.spec::before()', - action: 'continue' - }); - }) - ); + return DataJobsManagePage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("data-jobs-manage")) + .then(() => DataJobsManagePage.login()) + .then(() => cy.saveLocalStorage("data-jobs-manage")) + .then(() => DataJobsManagePage.deleteShortLivedTestJobsNoDeploy(true)) + .then(() => DataJobsManagePage.createLongLivedJobs("failing")) + .then(() => DataJobsManagePage.createShortLivedTestJobWithDeploy("v0")) + .then(() => + DataJobsManagePage.loadShortLivedTestJobFixtureWithDeploy("v0").then( + (loadedTestJob) => { + shortLivedTestJobWithDeployFixture = loadedTestJob; + + return cy.wrap({ + context: "manage::1::data-jobs.int.spec::before()", + action: "continue", + }); + }, + ), + ) + .then(() => DataJobsManagePage.createShortLivedTestJobsNoDeploy()) + .then(() => + DataJobsManagePage.loadShortLivedTestJobsFixtureNoDeploy().then( + (fixtures) => { + testJobsFixture = [fixtures[0], fixtures[1]]; + additionalTestJobFixture = fixtures[2]; + + return cy.wrap({ + context: "manage::2::data-jobs.int.spec::before()", + action: "continue", + }); + }, + ), + ); }); after(() => { - DataJobsManagePage.deleteShortLivedTestJobWithDeploy('v0'); - DataJobsManagePage.deleteShortLivedTestJobsNoDeploy(); + DataJobsManagePage.deleteShortLivedTestJobWithDeploy("v0"); + DataJobsManagePage.deleteShortLivedTestJobsNoDeploy(); - DataJobsManagePage.saveHarIfSupported(); + DataJobsManagePage.saveHarIfSupported(); }); beforeEach(() => { - cy.restoreLocalStorage('data-jobs-manage'); + cy.restoreLocalStorage("data-jobs-manage"); - DataJobsManagePage.wireUserSession(); - DataJobsManagePage.initInterceptors(); + DataJobsManagePage.wireUserSession(); + DataJobsManagePage.initInterceptors(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it('page is loaded and grid contains test jobs', () => { - dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); + describe("smoke", { tags: ["@smoke"] }, () => { + it("page is loaded and grid contains test jobs", () => { + dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); - dataJobsManagePage.getPageTitle().scrollIntoView().should('be.visible').invoke('text').invoke('trim').should('eq', 'Manage Data Jobs'); + dataJobsManagePage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .invoke("text") + .invoke("trim") + .should("eq", "Manage Data Jobs"); - dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.chooseQuickFilter(0); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); - dataJobsManagePage.getDataGrid().scrollIntoView().should('be.visible'); + dataJobsManagePage.getDataGrid().scrollIntoView().should("be.visible"); - testJobsFixture.forEach((testJob) => { - cy.log('Fixture for name: ' + testJob.job_name); + testJobsFixture.forEach((testJob) => { + cy.log("Fixture for name: " + testJob.job_name); - dataJobsManagePage.getDataGridCell(testJob.job_name).scrollIntoView().should('be.visible'); - }); + dataJobsManagePage + .getDataGridCell(testJob.job_name) + .scrollIntoView() + .should("be.visible"); }); + }); - it('grid filters by job name', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); + it("grid filters by job name", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); - dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.chooseQuickFilter(0); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name); - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); - it('grid searches by job name', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); + it("grid searches by job name", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); - dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.chooseQuickFilter(0); - dataJobsManagePage.searchByJobName(testJobsFixture[1].job_name); + dataJobsManagePage.searchByJobName(testJobsFixture[1].job_name); - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).should('not.exist'); + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .should("not.exist"); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - }); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + }); - it('disable/enable job', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); + it("disable/enable job", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); - dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.chooseQuickFilter(0); - const jobName = shortLivedTestJobWithDeployFixture.job_name; + const jobName = shortLivedTestJobWithDeployFixture.job_name; - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(shortLivedTestJobWithDeployFixture.job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + shortLivedTestJobWithDeployFixture.job_name.substring(0, 20), + ); - //Toggle job status twice, enable to disable and vice versa. - dataJobsManagePage.toggleJobStatus(shortLivedTestJobWithDeployFixture.job_name); - dataJobsManagePage.toggleJobStatus(shortLivedTestJobWithDeployFixture.job_name); - }); + //Toggle job status twice, enable to disable and vice versa. + dataJobsManagePage.toggleJobStatus( + shortLivedTestJobWithDeployFixture.job_name, + ); + dataJobsManagePage.toggleJobStatus( + shortLivedTestJobWithDeployFixture.job_name, + ); + }); }); - describe('extended', () => { - it('grid search parameter goes into URL', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); - - dataJobsManagePage.chooseQuickFilter(0); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - // verify 2 test rows visible - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // do search - dataJobsManagePage.searchByJobName(testJobsFixture[0].job_name); - - // verify 1 test row visible - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - - // verify url contains search value - dataJobsManagePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/manage/data-jobs', - queryParams: { - search: testJobsFixture[0].job_name, - jobName: testJobsFixture[0].job_name.substring(0, 20), - deploymentStatus: 'all' - } - }); - - // clear search with clear() method - dataJobsManagePage.clearSearchField(); - - // verify 2 test rows visible - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // verify url does not contain search value - dataJobsManagePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/manage/data-jobs', - queryParams: { - jobName: testJobsFixture[0].job_name.substring(0, 20), - deploymentStatus: 'all' - } - }); - }); - - it('grid search perform search when URL contains search parameter', () => { - // navigate with search value in URL - dataJobsManagePage = DataJobsManagePage.navigateToDataJobUrl(`/manage/data-jobs?search=${testJobsFixture[1].job_name}`); - - dataJobsManagePage.waitForGridToLoad(null); - - dataJobsManagePage.chooseQuickFilter(0); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - // verify url contains search value - dataJobsManagePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/manage/data-jobs', - queryParams: { - search: testJobsFixture[1].job_name, - jobName: testJobsFixture[0].job_name.substring(0, 20), - deploymentStatus: 'all' - } - }); - - // verify 1 test row visible - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).should('not.exist'); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // clear search with button - dataJobsManagePage.clearSearchFieldWithButton(); - - // verify 2 test rows visible - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).scrollIntoView().should('be.visible'); - - // verify url does not contain search value - dataJobsManagePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/manage/data-jobs', - queryParams: { - jobName: testJobsFixture[0].job_name.substring(0, 20), - deploymentStatus: 'all' - } - }); - }); - - it('refresh shows newly created job', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); - - dataJobsManagePage.chooseQuickFilter(0); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).should('have.text', testJobsFixture[0].job_name); - - dataJobsManagePage.getDataGridCell(additionalTestJobFixture.job_name).should('not.exist'); - - DataJobsManagePage.createAdditionalShortLivedTestJobsNoDeploy(); - - dataJobsManagePage.refreshDataGrid(); - - dataJobsManagePage.getDataGridCell(additionalTestJobFixture.job_name).should('have.text', additionalTestJobFixture.job_name); - }); - - it('click on edit button opens new page with Job details', () => { - dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); - - dataJobsManagePage.chooseQuickFilter(0); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - dataJobsManagePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); - - const dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); - - dataJobManageDetailsPage.getPageTitle().scrollIntoView().should('be.visible').invoke('text').invoke('trim').should('eq', `Data Job: ${testJobsFixture[0].job_name}`); - - dataJobManageDetailsPage.getDescription().scrollIntoView().should('be.visible').should('contain.text', testJobsFixture[0].description.split(' ').slice(0, descriptionWordsBeforeTruncate).join(' ')); - - dataJobManageDetailsPage.getSchedule().scrollIntoView().should('be.visible'); - - dataJobManageDetailsPage.getDeploymentStatus('not-deployed').scrollIntoView().should('be.visible').should('have.text', 'Not Deployed'); - }); - - it('quick filters', () => { - // Disable data job before test start - DataJobsManagePage.changeLongLivedJobStatus('failing', true) - .then(() => DataJobsManagePage.changeShortLivedTestJobWithDeployStatus('v0', false)) - .then(() => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); - - dataJobsManagePage.waitForClickThinkingTime(); - dataJobsManagePage.chooseQuickFilter(0); - - dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { - for (const icon of Array.from($icons)) { - cy.wrap(icon).invoke('attr', 'data-cy').should('match', new RegExp('data-pipelines-job-(enabled|disabled|not-deployed)')); - } - }); - - dataJobsManagePage.waitForClickThinkingTime(); - dataJobsManagePage.chooseQuickFilter(1); - - dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { - for (const icon of Array.from($icons)) { - cy.wrap(icon).should('have.attr', 'data-cy', 'data-pipelines-job-enabled'); - } - }); - - dataJobsManagePage.waitForClickThinkingTime(); - dataJobsManagePage.chooseQuickFilter(2); - - dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { - for (const icon of Array.from($icons)) { - cy.wrap(icon).should('have.attr', 'data-cy', 'data-pipelines-job-disabled'); - } - }); - - dataJobsManagePage.waitForClickThinkingTime(); - dataJobsManagePage.chooseQuickFilter(3); - - dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { - for (const icon of Array.from($icons)) { - cy.wrap(icon).should('have.attr', 'data-cy', 'data-pipelines-job-not-deployed'); - } - }); - - // Enable data job after job end - DataJobsManagePage.changeShortLivedTestJobWithDeployStatus('v0', true); - }); - }); - - it('filter description data jobs', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); - - dataJobsManagePage.chooseQuickFilter(0); - - // show panel for show/hide columns - dataJobsManagePage.toggleColumnShowHidePanel(); - - // verify column is not checked in toggling menu - dataJobsManagePage.getDataGridColumnShowHideOption('Description').should('exist').should('not.be.checked'); - - // verify header cell for column is not rendered - dataJobsManagePage.getDataGridHeaderCell('Description').should('have.length', 0); - - // toggle column to render - dataJobsManagePage.checkColumnShowHideOption('Description'); - - // verify column is checked in toggling menu - dataJobsManagePage.getHeaderColumnDescriptionName().should('exist'); - - // filter by job description because - dataJobsManagePage.filterByJobDescription('Test description 1'); - - // verify url contains description value - dataJobsManagePage.getCurrentUrl().should('match', new RegExp(`\\/manage\\/data-jobs\\?description=Test%20description%201&deploymentStatus=all$`)); - - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); - - it('perform filtering by description when URL contains description parameter', () => { - // navigate with search value in URL - dataJobsManagePage = DataJobsManagePage.navigateToDataJobUrl(`/manage/data-jobs?deploymentEnabled=enabled&description=Test%20description%201`); - - dataJobsManagePage.chooseQuickFilter(0); - dataJobsManagePage.waitForGridToLoad(null); - - // show panel for show/hide columns - dataJobsManagePage.toggleColumnShowHidePanel(); - - // toggle column to render - dataJobsManagePage.checkColumnShowHideOption('Description'); - - // verify url contains search value - dataJobsManagePage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true - }) - .should('deep.equal', { - pathSegment: '/manage/data-jobs', - queryParams: { - deploymentStatus: 'all', - description: 'Test%20description%201' - } - }); - - // verify 1 test row visible - dataJobsManagePage.getDataGridCell(testJobsFixture[0].job_name).scrollIntoView().should('be.visible'); - - dataJobsManagePage.getDataGridCell(testJobsFixture[1].job_name).should('not.exist'); - }); - - it('show/hide column when toggling from menu', () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); - - // show panel for show/hide columns - dataJobsManagePage.toggleColumnShowHidePanel(); - - // verify correct options are rendered - dataJobsManagePage.getDataGridColumnShowHideOptionsValues().should('have.length', 11).invoke('join', ',').should('eq', 'Description,Deployment Status,Python Version,Last Execution Duration,Success rate,Next run (UTC),Last Deployed (UTC),Last Deployed By,Notifications,Source,Logs'); - - // verify column is not checked in toggling menu - dataJobsManagePage.getDataGridColumnShowHideOption('Notifications').should('exist').should('not.be.checked'); - - // verify header cell for column is not rendered - dataJobsManagePage.getDataGridHeaderCell('Notifications').should('have.length', 0); - - // toggle column to render - dataJobsManagePage.checkColumnShowHideOption('Notifications'); - - // verify column is checked in toggling menu - dataJobsManagePage.getDataGridColumnShowHideOption('Notifications').should('exist').should('be.checked'); - - // verify header cell for column is rendered - dataJobsManagePage.getDataGridHeaderCell('Notifications').should('have.length', 1); - - // toggle column to hide - dataJobsManagePage.uncheckColumnShowHideOption('Notifications'); - - // verify column is not checked in toggling menu - dataJobsManagePage.getDataGridColumnShowHideOption('Notifications').should('exist').should('not.be.checked'); - - // verify header cell for column is not rendered - dataJobsManagePage.getDataGridHeaderCell('Notifications').should('have.length', 0); - - // hide panel for show/hide columns - dataJobsManagePage.toggleColumnShowHidePanel(); - }); - - it('execute now', () => { + describe("extended", () => { + it("grid search parameter goes into URL", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + // verify 2 test rows visible + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // do search + dataJobsManagePage.searchByJobName(testJobsFixture[0].job_name); + + // verify 1 test row visible + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + + // verify url contains search value + dataJobsManagePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/manage/data-jobs", + queryParams: { + search: testJobsFixture[0].job_name, + jobName: testJobsFixture[0].job_name.substring(0, 20), + deploymentStatus: "all", + }, + }); + + // clear search with clear() method + dataJobsManagePage.clearSearchField(); + + // verify 2 test rows visible + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // verify url does not contain search value + dataJobsManagePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/manage/data-jobs", + queryParams: { + jobName: testJobsFixture[0].job_name.substring(0, 20), + deploymentStatus: "all", + }, + }); + }); + + it("grid search perform search when URL contains search parameter", () => { + // navigate with search value in URL + dataJobsManagePage = DataJobsManagePage.navigateToDataJobUrl( + `/manage/data-jobs?search=${testJobsFixture[1].job_name}`, + ); + + dataJobsManagePage.waitForGridToLoad(null); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + // verify url contains search value + dataJobsManagePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/manage/data-jobs", + queryParams: { + search: testJobsFixture[1].job_name, + jobName: testJobsFixture[0].job_name.substring(0, 20), + deploymentStatus: "all", + }, + }); + + // verify 1 test row visible + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .should("not.exist"); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // clear search with button + dataJobsManagePage.clearSearchFieldWithButton(); + + // verify 2 test rows visible + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .scrollIntoView() + .should("be.visible"); + + // verify url does not contain search value + dataJobsManagePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/manage/data-jobs", + queryParams: { + jobName: testJobsFixture[0].job_name.substring(0, 20), + deploymentStatus: "all", + }, + }); + }); + + it("refresh shows newly created job", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .should("have.text", testJobsFixture[0].job_name); + + dataJobsManagePage + .getDataGridCell(additionalTestJobFixture.job_name) + .should("not.exist"); + + DataJobsManagePage.createAdditionalShortLivedTestJobsNoDeploy(); + + dataJobsManagePage.refreshDataGrid(); + + dataJobsManagePage + .getDataGridCell(additionalTestJobFixture.job_name) + .should("have.text", additionalTestJobFixture.job_name); + }); + + it("click on edit button opens new page with Job details", () => { + dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + dataJobsManagePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); + + const dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); + + dataJobManageDetailsPage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .invoke("text") + .invoke("trim") + .should("eq", `Data Job: ${testJobsFixture[0].job_name}`); + + dataJobManageDetailsPage + .getDescription() + .scrollIntoView() + .should("be.visible") + .should( + "contain.text", + testJobsFixture[0].description + .split(" ") + .slice(0, descriptionWordsBeforeTruncate) + .join(" "), + ); + + dataJobManageDetailsPage + .getSchedule() + .scrollIntoView() + .should("be.visible"); + + dataJobManageDetailsPage + .getDeploymentStatus("not-deployed") + .scrollIntoView() + .should("be.visible") + .should("have.text", "Not Deployed"); + }); + + it("quick filters", () => { + // Disable data job before test start + DataJobsManagePage.changeLongLivedJobStatus("failing", true) + .then(() => + DataJobsManagePage.changeShortLivedTestJobWithDeployStatus( + "v0", + false, + ), + ) + .then(() => { dataJobsManagePage = DataJobsManagePage.navigateTo(); + dataJobsManagePage.waitForClickThinkingTime(); dataJobsManagePage.chooseQuickFilter(0); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(shortLivedTestJobWithDeployFixture.job_name.substring(0, 20)); - - DataJobsManagePage.waitForShortLivedTestJobWithDeployExecutionToComplete('v0'); - - dataJobsManagePage.executeDataJob(shortLivedTestJobWithDeployFixture.job_name); - - // TODO better handling toast message. If tests are executed immediately it will return - // error 409 conflict and it will say that the job is already executing - dataJobsManagePage - .getToastTitle(10000) // Wait up to 10 seconds for Toast to show. - .should('exist') - .contains(/Data job Queued for execution|Failed, Data job is already executing/); - }); - - it(`execute now is disabled when Job doesn't have deployment`, () => { - dataJobsManagePage = DataJobsManagePage.navigateTo(); - - const jobName = testJobsFixture[0].job_name; - - dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon) + .invoke("attr", "data-cy") + .should( + "match", + new RegExp( + "data-pipelines-job-(enabled|disabled|not-deployed)", + ), + ); + } + }); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(jobName.substring(0, 20)); + dataJobsManagePage.waitForClickThinkingTime(); + dataJobsManagePage.chooseQuickFilter(1); + + dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).should( + "have.attr", + "data-cy", + "data-pipelines-job-enabled", + ); + } + }); - dataJobsManagePage.selectRow(jobName); + dataJobsManagePage.waitForClickThinkingTime(); + dataJobsManagePage.chooseQuickFilter(2); + + dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).should( + "have.attr", + "data-cy", + "data-pipelines-job-disabled", + ); + } + }); dataJobsManagePage.waitForClickThinkingTime(); + dataJobsManagePage.chooseQuickFilter(3); + + dataJobsManagePage.getDataGridStatusIcons().then(($icons) => { + for (const icon of Array.from($icons)) { + cy.wrap(icon).should( + "have.attr", + "data-cy", + "data-pipelines-job-not-deployed", + ); + } + }); - dataJobsManagePage.getExecuteNowGridButton().should('be.disabled'); - }); + // Enable data job after job end + DataJobsManagePage.changeShortLivedTestJobWithDeployStatus( + "v0", + true, + ); + }); + }); + + it("filter description data jobs", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); + + dataJobsManagePage.chooseQuickFilter(0); + + // show panel for show/hide columns + dataJobsManagePage.toggleColumnShowHidePanel(); + + // verify column is not checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption("Description") + .should("exist") + .should("not.be.checked"); + + // verify header cell for column is not rendered + dataJobsManagePage + .getDataGridHeaderCell("Description") + .should("have.length", 0); + + // toggle column to render + dataJobsManagePage.checkColumnShowHideOption("Description"); + + // verify column is checked in toggling menu + dataJobsManagePage.getHeaderColumnDescriptionName().should("exist"); + + // filter by job description because + dataJobsManagePage.filterByJobDescription("Test description 1"); + + // verify url contains description value + dataJobsManagePage + .getCurrentUrl() + .should( + "match", + new RegExp( + `\\/manage\\/data-jobs\\?description=Test%20description%201&deploymentStatus=all$`, + ), + ); + + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); + + it("perform filtering by description when URL contains description parameter", () => { + // navigate with search value in URL + dataJobsManagePage = DataJobsManagePage.navigateToDataJobUrl( + `/manage/data-jobs?deploymentEnabled=enabled&description=Test%20description%201`, + ); + + dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.waitForGridToLoad(null); + + // show panel for show/hide columns + dataJobsManagePage.toggleColumnShowHidePanel(); + + // toggle column to render + dataJobsManagePage.checkColumnShowHideOption("Description"); + + // verify url contains search value + dataJobsManagePage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + }) + .should("deep.equal", { + pathSegment: "/manage/data-jobs", + queryParams: { + deploymentStatus: "all", + description: "Test%20description%201", + }, + }); + + // verify 1 test row visible + dataJobsManagePage + .getDataGridCell(testJobsFixture[0].job_name) + .scrollIntoView() + .should("be.visible"); + + dataJobsManagePage + .getDataGridCell(testJobsFixture[1].job_name) + .should("not.exist"); + }); + + it("show/hide column when toggling from menu", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); + + // show panel for show/hide columns + dataJobsManagePage.toggleColumnShowHidePanel(); + + // verify correct options are rendered + dataJobsManagePage + .getDataGridColumnShowHideOptionsValues() + .should("have.length", 11) + .invoke("join", ",") + .should( + "eq", + "Description,Deployment Status,Python Version,Last Execution Duration,Success rate,Next run (UTC),Last Deployed (UTC),Last Deployed By,Notifications,Source,Logs", + ); + + // verify column is not checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption("Notifications") + .should("exist") + .should("not.be.checked"); + + // verify header cell for column is not rendered + dataJobsManagePage + .getDataGridHeaderCell("Notifications") + .should("have.length", 0); + + // toggle column to render + dataJobsManagePage.checkColumnShowHideOption("Notifications"); + + // verify column is checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption("Notifications") + .should("exist") + .should("be.checked"); + + // verify header cell for column is rendered + dataJobsManagePage + .getDataGridHeaderCell("Notifications") + .should("have.length", 1); + + // toggle column to hide + dataJobsManagePage.uncheckColumnShowHideOption("Notifications"); + + // verify column is not checked in toggling menu + dataJobsManagePage + .getDataGridColumnShowHideOption("Notifications") + .should("exist") + .should("not.be.checked"); + + // verify header cell for column is not rendered + dataJobsManagePage + .getDataGridHeaderCell("Notifications") + .should("have.length", 0); + + // hide panel for show/hide columns + dataJobsManagePage.toggleColumnShowHidePanel(); + }); + + it("execute now", () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + shortLivedTestJobWithDeployFixture.job_name.substring(0, 20), + ); + + DataJobsManagePage.waitForShortLivedTestJobWithDeployExecutionToComplete( + "v0", + ); + + dataJobsManagePage.executeDataJob( + shortLivedTestJobWithDeployFixture.job_name, + ); + + // TODO better handling toast message. If tests are executed immediately it will return + // error 409 conflict and it will say that the job is already executing + dataJobsManagePage + .getToastTitle(10000) // Wait up to 10 seconds for Toast to show. + .should("exist") + .contains( + /Data job Queued for execution|Failed, Data job is already executing/, + ); + }); + + it(`execute now is disabled when Job doesn't have deployment`, () => { + dataJobsManagePage = DataJobsManagePage.navigateTo(); + + const jobName = testJobsFixture[0].job_name; + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName(jobName.substring(0, 20)); + + dataJobsManagePage.selectRow(jobName); + + dataJobsManagePage.waitForClickThinkingTime(); + + dataJobsManagePage.getExecuteNowGridButton().should("be.disabled"); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/details/data-job-details.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/details/data-job-details.spec.js index e7a46c66f5..5ce59528ea 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/details/data-job-details.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/details/data-job-details.spec.js @@ -5,12 +5,15 @@ /// -import { compareDatesASC } from '../../../../../plugins/helpers/job-helpers.plugins'; +import { compareDatesASC } from "../../../../../plugins/helpers/job-helpers.plugins"; -import { DataJobsManagePage } from '../../../../../support/pages/manage/data-jobs/data-jobs.po'; -import { DataJobManageDetailsPage } from '../../../../../support/pages/manage/data-jobs/details/data-job-details.po'; +import { DataJobsManagePage } from "../../../../../support/pages/manage/data-jobs/data-jobs.po"; +import { DataJobManageDetailsPage } from "../../../../../support/pages/manage/data-jobs/details/data-job-details.po"; -describe('Data Job Manage Details Page', { tags: ['@dataPipelines', '@manageDataJobDetails', '@manage'] }, () => { +describe( + "Data Job Manage Details Page", + { tags: ["@dataPipelines", "@manageDataJobDetails", "@manage"] }, + () => { const descriptionWordsBeforeTruncate = 12; /** @@ -35,202 +38,300 @@ describe('Data Job Manage Details Page', { tags: ['@dataPipelines', '@manageData let longLivedFailingJobFixture; before(() => { - return DataJobManageDetailsPage.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('data-job-manage-details')) - .then(() => DataJobManageDetailsPage.login()) - .then(() => cy.saveLocalStorage('data-job-manage-details')) - .then(() => DataJobManageDetailsPage.deleteShortLivedTestJobsNoDeploy(true)) - .then(() => DataJobManageDetailsPage.createLongLivedJobs('failing')) - .then(() => - DataJobManageDetailsPage.provideExecutionsForLongLivedJobs({ - job: 'failing' - }) - ) - .then(() => - DataJobManageDetailsPage.loadLongLivedFailingJobFixture().then((loadedTestJob) => { - longLivedFailingJobFixture = loadedTestJob; - - return cy.wrap({ - context: 'manage::1::data-job-details.spec::before()', - action: 'continue' - }); - }) - ) - .then(() => DataJobManageDetailsPage.createShortLivedTestJobWithDeploy('v1')) - .then(() => - DataJobManageDetailsPage.loadShortLivedTestJobFixtureWithDeploy('v1').then((loadedTestJob) => { - shortLivedTestJobWithDeployFixture = loadedTestJob; - - return cy.wrap({ - context: 'manage::2::data-job-details.spec::before()', - action: 'continue' - }); - }) - ) - .then(() => DataJobManageDetailsPage.createShortLivedTestJobsNoDeploy()) - .then(() => - DataJobManageDetailsPage.loadShortLivedTestJobsFixtureNoDeploy().then((fixtures) => { - testJobsFixture = [fixtures[0], fixtures[1]]; - additionalTestJobFixture = fixtures[2]; - - return cy.wrap({ - context: 'manage::3::data-job-details.spec::before()', - action: 'continue' - }); - }) - ); + return DataJobManageDetailsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("data-job-manage-details")) + .then(() => DataJobManageDetailsPage.login()) + .then(() => cy.saveLocalStorage("data-job-manage-details")) + .then(() => + DataJobManageDetailsPage.deleteShortLivedTestJobsNoDeploy(true), + ) + .then(() => DataJobManageDetailsPage.createLongLivedJobs("failing")) + .then(() => + DataJobManageDetailsPage.provideExecutionsForLongLivedJobs({ + job: "failing", + }), + ) + .then(() => + DataJobManageDetailsPage.loadLongLivedFailingJobFixture().then( + (loadedTestJob) => { + longLivedFailingJobFixture = loadedTestJob; + + return cy.wrap({ + context: "manage::1::data-job-details.spec::before()", + action: "continue", + }); + }, + ), + ) + .then(() => + DataJobManageDetailsPage.createShortLivedTestJobWithDeploy("v1"), + ) + .then(() => + DataJobManageDetailsPage.loadShortLivedTestJobFixtureWithDeploy( + "v1", + ).then((loadedTestJob) => { + shortLivedTestJobWithDeployFixture = loadedTestJob; + + return cy.wrap({ + context: "manage::2::data-job-details.spec::before()", + action: "continue", + }); + }), + ) + .then(() => DataJobManageDetailsPage.createShortLivedTestJobsNoDeploy()) + .then(() => + DataJobManageDetailsPage.loadShortLivedTestJobsFixtureNoDeploy().then( + (fixtures) => { + testJobsFixture = [fixtures[0], fixtures[1]]; + additionalTestJobFixture = fixtures[2]; + + return cy.wrap({ + context: "manage::3::data-job-details.spec::before()", + action: "continue", + }); + }, + ), + ); }); after(() => { - DataJobManageDetailsPage.deleteShortLivedTestJobWithDeploy('v1'); - DataJobManageDetailsPage.deleteShortLivedTestJobsNoDeploy(); + DataJobManageDetailsPage.deleteShortLivedTestJobWithDeploy("v1"); + DataJobManageDetailsPage.deleteShortLivedTestJobsNoDeploy(); - DataJobManageDetailsPage.saveHarIfSupported(); + DataJobManageDetailsPage.saveHarIfSupported(); }); beforeEach(() => { - cy.restoreLocalStorage('data-job-manage-details'); + cy.restoreLocalStorage("data-job-manage-details"); - DataJobManageDetailsPage.wireUserSession(); - DataJobManageDetailsPage.initInterceptors(); + DataJobManageDetailsPage.wireUserSession(); + DataJobManageDetailsPage.initInterceptors(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it('should verify will open job details', () => { - const dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); - - dataJobsManagePage.chooseQuickFilter(0); - - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(shortLivedTestJobWithDeployFixture.job_name.substring(0, 20)); - - dataJobsManagePage.openJobDetails(shortLivedTestJobWithDeployFixture.team, shortLivedTestJobWithDeployFixture.job_name); - - dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); - - dataJobManageDetailsPage.getPageTitle().invoke('text').invoke('trim').should('eq', `Data Job: ${shortLivedTestJobWithDeployFixture.job_name}`); - }); - - it('disable/enable job', () => { - dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo(shortLivedTestJobWithDeployFixture.team, shortLivedTestJobWithDeployFixture.job_name); - - //Toggle job status twice, enable to disable and vice versa. - dataJobManageDetailsPage.toggleJobStatus(shortLivedTestJobWithDeployFixture.job_name); - dataJobManageDetailsPage.toggleJobStatus(shortLivedTestJobWithDeployFixture.job_name); - }); - - it('execute now', () => { - dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo(shortLivedTestJobWithDeployFixture.team, shortLivedTestJobWithDeployFixture.job_name); - - DataJobManageDetailsPage.waitForShortLivedTestJobWithDeployExecutionToComplete('v1'); - - dataJobManageDetailsPage.executeNow(); - - dataJobManageDetailsPage - .getToastTitle(10000) - .should('exist') - .contains(/Data job Queued for execution|Failed, Data job is already executing/); - }); - - it('delete job', () => { - DataJobsManagePage.createAdditionalShortLivedTestJobsNoDeploy(); - - dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo(additionalTestJobFixture.team, additionalTestJobFixture.job_name); - - dataJobManageDetailsPage.openActionDropdown(); - - dataJobManageDetailsPage.deleteJob(); - - dataJobManageDetailsPage - .getToastTitle(20000) // Wait up to 20 seconds for the job to be deleted. - .should('contain.text', 'Data job delete completed'); - - const dataJobsManagePage = DataJobsManagePage.getPage(); - - dataJobsManagePage.waitForGridDataLoad(); - - dataJobsManagePage.chooseQuickFilter(0); - - dataJobsManagePage - .getDataGridCell(additionalTestJobFixture.job_name, 10000) // Wait up to 10 seconds for the jobs list to show. - .should('not.exist'); - }); + describe("smoke", { tags: ["@smoke"] }, () => { + it("should verify will open job details", () => { + const dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + shortLivedTestJobWithDeployFixture.job_name.substring(0, 20), + ); + + dataJobsManagePage.openJobDetails( + shortLivedTestJobWithDeployFixture.team, + shortLivedTestJobWithDeployFixture.job_name, + ); + + dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); + + dataJobManageDetailsPage + .getPageTitle() + .invoke("text") + .invoke("trim") + .should( + "eq", + `Data Job: ${shortLivedTestJobWithDeployFixture.job_name}`, + ); + }); + + it("disable/enable job", () => { + dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo( + shortLivedTestJobWithDeployFixture.team, + shortLivedTestJobWithDeployFixture.job_name, + ); + + //Toggle job status twice, enable to disable and vice versa. + dataJobManageDetailsPage.toggleJobStatus( + shortLivedTestJobWithDeployFixture.job_name, + ); + dataJobManageDetailsPage.toggleJobStatus( + shortLivedTestJobWithDeployFixture.job_name, + ); + }); + + it("execute now", () => { + dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo( + shortLivedTestJobWithDeployFixture.team, + shortLivedTestJobWithDeployFixture.job_name, + ); + + DataJobManageDetailsPage.waitForShortLivedTestJobWithDeployExecutionToComplete( + "v1", + ); + + dataJobManageDetailsPage.executeNow(); + + dataJobManageDetailsPage + .getToastTitle(10000) + .should("exist") + .contains( + /Data job Queued for execution|Failed, Data job is already executing/, + ); + }); + + it("delete job", () => { + DataJobsManagePage.createAdditionalShortLivedTestJobsNoDeploy(); + + dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo( + additionalTestJobFixture.team, + additionalTestJobFixture.job_name, + ); + + dataJobManageDetailsPage.openActionDropdown(); + + dataJobManageDetailsPage.deleteJob(); + + dataJobManageDetailsPage + .getToastTitle(20000) // Wait up to 20 seconds for the job to be deleted. + .should("contain.text", "Data job delete completed"); + + const dataJobsManagePage = DataJobsManagePage.getPage(); + + dataJobsManagePage.waitForGridDataLoad(); + + dataJobsManagePage.chooseQuickFilter(0); + + dataJobsManagePage + .getDataGridCell(additionalTestJobFixture.job_name, 10000) // Wait up to 10 seconds for the jobs list to show. + .should("not.exist"); + }); }); - describe('extended', () => { - it('edit job description', () => { - let newDescription = 'Test if changing the description is working'; - - const dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); - - dataJobsManagePage.chooseQuickFilter(0); + describe("extended", () => { + it("edit job description", () => { + let newDescription = "Test if changing the description is working"; + + const dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); + + dataJobsManagePage.chooseQuickFilter(0); + + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + testJobsFixture[0].job_name.substring(0, 20), + ); + + dataJobsManagePage.openJobDetails( + testJobsFixture[0].team, + testJobsFixture[0].job_name, + ); + + dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); + + dataJobManageDetailsPage.openDescription(); + + dataJobManageDetailsPage.enterDescriptionDetails(newDescription); + + dataJobManageDetailsPage.saveDescription(); + + dataJobManageDetailsPage + .getDescription() + .scrollIntoView() + .should("be.visible") + .should( + "contain.text", + newDescription + .split(" ") + .slice(0, descriptionWordsBeforeTruncate) + .join(" "), + ); + }); + + it("download job key", () => { + dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo( + shortLivedTestJobWithDeployFixture.team, + shortLivedTestJobWithDeployFixture.job_name, + ); + + dataJobManageDetailsPage.openActionDropdown(); + + dataJobManageDetailsPage.downloadJobKey(); + + dataJobManageDetailsPage + .readFile( + "downloadsFolder", + `${shortLivedTestJobWithDeployFixture.job_name}.keytab`, + ) + .should("exist"); + }); + + it("executions timeline", () => { + const jobName = longLivedFailingJobFixture.job_name; + const teamName = longLivedFailingJobFixture.team; + + dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo( + teamName, + jobName, + ); + + dataJobManageDetailsPage + .waitForGetExecutionsReqInterceptor() + .then((interception) => { + const executionsResponse = []; + const content = interception?.response?.body?.data?.content; + + if (content) { + executionsResponse.push(...content); + } + + const lastExecutions = executionsResponse + .sort((left, right) => compareDatesASC(left, right)) + .slice( + executionsResponse.length > 5 + ? executionsResponse.length - 5 + : 0, + ); + + const lastExecutionsSize = lastExecutions.length; + const lastExecution = lastExecutions[lastExecutionsSize - 1]; + const timelineSize = lastExecutionsSize + 1; // +1 next execution - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(testJobsFixture[0].job_name.substring(0, 20)); - - dataJobsManagePage.openJobDetails(testJobsFixture[0].team, testJobsFixture[0].job_name); - - dataJobManageDetailsPage = DataJobManageDetailsPage.getPage(); - - dataJobManageDetailsPage.openDescription(); - - dataJobManageDetailsPage.enterDescriptionDetails(newDescription); - - dataJobManageDetailsPage.saveDescription(); - - dataJobManageDetailsPage.getDescription().scrollIntoView().should('be.visible').should('contain.text', newDescription.split(' ').slice(0, descriptionWordsBeforeTruncate).join(' ')); - }); - - it('download job key', () => { - dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo(shortLivedTestJobWithDeployFixture.team, shortLivedTestJobWithDeployFixture.job_name); - - dataJobManageDetailsPage.openActionDropdown(); - - dataJobManageDetailsPage.downloadJobKey(); - - dataJobManageDetailsPage.readFile('downloadsFolder', `${shortLivedTestJobWithDeployFixture.job_name}.keytab`).should('exist'); - }); - - it('executions timeline', () => { - const jobName = longLivedFailingJobFixture.job_name; - const teamName = longLivedFailingJobFixture.team; - - dataJobManageDetailsPage = DataJobManageDetailsPage.navigateTo(teamName, jobName); - - dataJobManageDetailsPage.waitForGetExecutionsReqInterceptor().then((interception) => { - const executionsResponse = []; - const content = interception?.response?.body?.data?.content; - - if (content) { - executionsResponse.push(...content); - } - - const lastExecutions = executionsResponse.sort((left, right) => compareDatesASC(left, right)).slice(executionsResponse.length > 5 ? executionsResponse.length - 5 : 0); - - const lastExecutionsSize = lastExecutions.length; - const lastExecution = lastExecutions[lastExecutionsSize - 1]; - const timelineSize = lastExecutionsSize + 1; // +1 next execution - - dataJobManageDetailsPage.getExecutionsSteps().should('have.length', timelineSize); - - for (const execution of lastExecutions) { - const executionTimelineSelector = `[data-cy=${execution.id}]`; - - const executionStartedTime = dataJobManageDetailsPage.formatDateTimeFromISOToExecutionsTimeline(execution.startTime); - dataJobManageDetailsPage.getExecutionStepStartedTile(executionTimelineSelector).invoke('trim').should('eq', `Started ${executionStartedTime}`); - - if (execution?.type?.toLowerCase() === 'manual') { - dataJobManageDetailsPage.getExecutionStepManualTriggerer(executionTimelineSelector).should('be.visible'); - } - - if (execution?.status?.toLowerCase() !== 'running' && execution?.status?.toLowerCase() !== 'submitted') { - dataJobManageDetailsPage.getExecutionStepStatusIcon(executionTimelineSelector, execution.status).should('exist'); - - const executionEndTime = dataJobManageDetailsPage.formatDateTimeFromISOToExecutionsTimeline(execution.endTime); - dataJobManageDetailsPage.getExecutionStepEndedTile(executionTimelineSelector).invoke('trim').should('eq', `Ended ${executionEndTime}`); - } - } - }); - }); + dataJobManageDetailsPage + .getExecutionsSteps() + .should("have.length", timelineSize); + + for (const execution of lastExecutions) { + const executionTimelineSelector = `[data-cy=${execution.id}]`; + + const executionStartedTime = + dataJobManageDetailsPage.formatDateTimeFromISOToExecutionsTimeline( + execution.startTime, + ); + dataJobManageDetailsPage + .getExecutionStepStartedTile(executionTimelineSelector) + .invoke("trim") + .should("eq", `Started ${executionStartedTime}`); + + if (execution?.type?.toLowerCase() === "manual") { + dataJobManageDetailsPage + .getExecutionStepManualTriggerer(executionTimelineSelector) + .should("be.visible"); + } + + if ( + execution?.status?.toLowerCase() !== "running" && + execution?.status?.toLowerCase() !== "submitted" + ) { + dataJobManageDetailsPage + .getExecutionStepStatusIcon( + executionTimelineSelector, + execution.status, + ) + .should("exist"); + + const executionEndTime = + dataJobManageDetailsPage.formatDateTimeFromISOToExecutionsTimeline( + execution.endTime, + ); + dataJobManageDetailsPage + .getExecutionStepEndedTile(executionTimelineSelector) + .invoke("trim") + .should("eq", `Ended ${executionEndTime}`); + } + } + }); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/executions/data-job-executions.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/executions/data-job-executions.spec.js index 9bc2f3ad3e..fe711df78a 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/executions/data-job-executions.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/manage/data-jobs/executions/data-job-executions.spec.js @@ -5,10 +5,13 @@ /// -import { DataJobsManagePage } from '../../../../../support/pages/manage/data-jobs/data-jobs.po'; -import { DataJobManageExecutionsPage } from '../../../../../support/pages/manage/data-jobs/executions/data-job-executions.po'; +import { DataJobsManagePage } from "../../../../../support/pages/manage/data-jobs/data-jobs.po"; +import { DataJobManageExecutionsPage } from "../../../../../support/pages/manage/data-jobs/executions/data-job-executions.po"; -describe('Data Job Manage Executions Page', { tags: ['@dataPipelines', '@manageDataJobExecutions', '@manage'] }, () => { +describe( + "Data Job Manage Executions Page", + { tags: ["@dataPipelines", "@manageDataJobExecutions", "@manage"] }, + () => { /** * @type {{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}} */ @@ -19,1664 +22,1985 @@ describe('Data Job Manage Executions Page', { tags: ['@dataPipelines', '@manageD let longLivedFailingJobFixture; before(() => { - return DataJobManageExecutionsPage.recordHarIfSupported() - .then(() => cy.clearLocalStorageSnapshot('data-job-manage-executions')) - .then(() => DataJobManageExecutionsPage.login()) - .then(() => cy.saveLocalStorage('data-job-manage-executions')) - .then(() => DataJobManageExecutionsPage.createLongLivedJobs('failing')) - .then(() => DataJobManageExecutionsPage.provideExecutionsForLongLivedJobs({ job: 'failing', executions: 3 })) - .then(() => - DataJobManageExecutionsPage.loadLongLivedFailingJobFixture().then((loadedTestJob) => { - longLivedFailingJobFixture = loadedTestJob; - - return cy.wrap({ - context: 'manage::1::data-job-executions.spec::before()', - action: 'continue' - }); - }) - ) - .then(() => DataJobManageExecutionsPage.createShortLivedTestJobWithDeploy('v2')) - .then(() => DataJobManageExecutionsPage.provideExecutionsForShortLivedTestJobWithDeploy('v2')) - .then(() => - DataJobManageExecutionsPage.loadShortLivedTestJobFixtureWithDeploy('v2').then((loadedTestJob) => { - shortLivedTestJobWithDeployFixture = loadedTestJob; - - return cy.wrap({ - context: 'manage::2::data-job-executions.spec::before()', - action: 'continue' - }); - }) - ); + return DataJobManageExecutionsPage.recordHarIfSupported() + .then(() => cy.clearLocalStorageSnapshot("data-job-manage-executions")) + .then(() => DataJobManageExecutionsPage.login()) + .then(() => cy.saveLocalStorage("data-job-manage-executions")) + .then(() => DataJobManageExecutionsPage.createLongLivedJobs("failing")) + .then(() => + DataJobManageExecutionsPage.provideExecutionsForLongLivedJobs({ + job: "failing", + executions: 3, + }), + ) + .then(() => + DataJobManageExecutionsPage.loadLongLivedFailingJobFixture().then( + (loadedTestJob) => { + longLivedFailingJobFixture = loadedTestJob; + + return cy.wrap({ + context: "manage::1::data-job-executions.spec::before()", + action: "continue", + }); + }, + ), + ) + .then(() => + DataJobManageExecutionsPage.createShortLivedTestJobWithDeploy("v2"), + ) + .then(() => + DataJobManageExecutionsPage.provideExecutionsForShortLivedTestJobWithDeploy( + "v2", + ), + ) + .then(() => + DataJobManageExecutionsPage.loadShortLivedTestJobFixtureWithDeploy( + "v2", + ).then((loadedTestJob) => { + shortLivedTestJobWithDeployFixture = loadedTestJob; + + return cy.wrap({ + context: "manage::2::data-job-executions.spec::before()", + action: "continue", + }); + }), + ); }); after(() => { - DataJobManageExecutionsPage.deleteShortLivedTestJobWithDeploy('v2'); + DataJobManageExecutionsPage.deleteShortLivedTestJobWithDeploy("v2"); - DataJobManageExecutionsPage.saveHarIfSupported(); + DataJobManageExecutionsPage.saveHarIfSupported(); }); beforeEach(() => { - cy.restoreLocalStorage('data-job-manage-executions'); + cy.restoreLocalStorage("data-job-manage-executions"); - DataJobManageExecutionsPage.wireUserSession(); - DataJobManageExecutionsPage.initInterceptors(); + DataJobManageExecutionsPage.wireUserSession(); + DataJobManageExecutionsPage.initInterceptors(); }); - describe('smoke', { tags: ['@smoke'] }, () => { - it(`should open Details and verify Executions tab is displayed and navigates`, () => { - cy.log('Fixture for name: ' + longLivedFailingJobFixture.job_name); + describe("smoke", { tags: ["@smoke"] }, () => { + it(`should open Details and verify Executions tab is displayed and navigates`, () => { + cy.log("Fixture for name: " + longLivedFailingJobFixture.job_name); - const dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); + const dataJobsManagePage = DataJobsManagePage.navigateWithSideMenu(); - dataJobsManagePage.chooseQuickFilter(0); + dataJobsManagePage.chooseQuickFilter(0); - // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page - dataJobsManagePage.filterByJobName(longLivedFailingJobFixture.job_name.substring(0, 20)); + // filter by job name substring because there are a lot of jobs, and it could potentially be on second/third page + dataJobsManagePage.filterByJobName( + longLivedFailingJobFixture.job_name.substring(0, 20), + ); - dataJobsManagePage.openJobDetails(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); + dataJobsManagePage.openJobDetails( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); - const dataJobManageExecutionsPage = DataJobManageExecutionsPage.getPage(); + const dataJobManageExecutionsPage = + DataJobManageExecutionsPage.getPage(); - dataJobManageExecutionsPage.getDetailsTab().should('exist').should('have.class', 'active'); + dataJobManageExecutionsPage + .getDetailsTab() + .should("exist") + .should("have.class", "active"); - dataJobManageExecutionsPage.getExecutionsTab().should('exist').should('not.have.class', 'active'); + dataJobManageExecutionsPage + .getExecutionsTab() + .should("exist") + .should("not.have.class", "active"); - dataJobManageExecutionsPage.openExecutionsTab(); + dataJobManageExecutionsPage.openExecutionsTab(); - const dataJobExecutionsPage = DataJobManageExecutionsPage.getPage(); + const dataJobExecutionsPage = DataJobManageExecutionsPage.getPage(); - dataJobExecutionsPage.getDetailsTab().should('exist').should('not.have.class', 'active'); + dataJobExecutionsPage + .getDetailsTab() + .should("exist") + .should("not.have.class", "active"); - dataJobExecutionsPage.getExecutionsTab().should('exist').should('have.class', 'active'); + dataJobExecutionsPage + .getExecutionsTab() + .should("exist") + .should("have.class", "active"); - dataJobExecutionsPage.getDataGrid().should('exist'); - }); + dataJobExecutionsPage.getDataGrid().should("exist"); + }); + + it("should verify elements are rendered in DOM", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .should("contains.text", longLivedFailingJobFixture.job_name); + + dataJobExecutionsPage + .getDetailsTab() + .should("exist") + .should("not.have.class", "active"); + + dataJobExecutionsPage + .getExecutionsTab() + .should("exist") + .should("have.class", "active"); + + dataJobExecutionsPage.getExecuteOrCancelButton().should("exist"); + + dataJobExecutionsPage.getActionDropdownBtn().should("exist"); + + dataJobExecutionsPage.openActionDropdown(); + + dataJobExecutionsPage.getDeleteJobBtn().should("exist"); - it('should verify elements are rendered in DOM', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); + dataJobExecutionsPage.clickOnContentContainer(); - dataJobExecutionsPage.getPageTitle().scrollIntoView().should('be.visible').should('contains.text', longLivedFailingJobFixture.job_name); + dataJobExecutionsPage.waitForSmartDelay(); - dataJobExecutionsPage.getDetailsTab().should('exist').should('not.have.class', 'active'); + dataJobExecutionsPage.getTimePeriod().should("exist"); - dataJobExecutionsPage.getExecutionsTab().should('exist').should('have.class', 'active'); + dataJobExecutionsPage.getStatusChart().should("exist"); - dataJobExecutionsPage.getExecuteOrCancelButton().should('exist'); + dataJobExecutionsPage.getDurationChart().should("exist"); - dataJobExecutionsPage.getActionDropdownBtn().should('exist'); + dataJobExecutionsPage.getDataGrid().should("exist"); - dataJobExecutionsPage.openActionDropdown(); + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + }); - dataJobExecutionsPage.getDeleteJobBtn().should('exist'); + it("should verify start then cancel execution works", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + shortLivedTestJobWithDeployFixture.team, + shortLivedTestJobWithDeployFixture.job_name, + ); - dataJobExecutionsPage.clickOnContentContainer(); + DataJobManageExecutionsPage.waitForDataJobToNotHaveRunningExecution(); - dataJobExecutionsPage.waitForSmartDelay(); + dataJobExecutionsPage + .getExecutionsTab() + .should("exist") + .should("have.class", "active"); - dataJobExecutionsPage.getTimePeriod().should('exist'); + // Execute data job and check if the execution status after that is Running or Submitted + dataJobExecutionsPage.executeNow(true); + dataJobExecutionsPage + .getExecutionStatus() + .first() + .contains(/Running|Submitted/); - dataJobExecutionsPage.getStatusChart().should('exist'); + // Cancel data job execution and check if the status after that is Cancelled + dataJobExecutionsPage.cancelExecution(true); + dataJobExecutionsPage + .getExecutionStatus() + .first() + .should("contains.text", "Canceled"); + }); + }); - dataJobExecutionsPage.getDurationChart().should('exist'); + describe("extended", () => { + it("should verify on URL navigate to Executions will open the page", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + + dataJobExecutionsPage + .getDetailsTab() + .should("exist") + .should("not.have.class", "active"); + + dataJobExecutionsPage + .getExecutionsTab() + .should("exist") + .should("have.class", "active"); + + dataJobExecutionsPage.getDataGrid().should("exist"); + }); + + it("should verify time period is in correct format", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage + .getTimePeriod() + .invoke("text") + .invoke("trim") + .should( + "match", + new RegExp( + `^\\w+\\s\\d+,\\s\\d+,\\s\\d+:\\d+:\\d+\\s(AM|PM)\\sto\\s\\w+\\s\\d+,\\s\\d+,\\s\\d+:\\d+:\\d+\\s(AM|PM)$`, + ), + ); + }); + + it("should verify refresh button will show spinner and then load data", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + // verify before + dataJobExecutionsPage.getDataGrid().should("exist"); + dataJobExecutionsPage.getExecLoadingSpinner().should("not.exist"); + dataJobExecutionsPage.getDataGridSpinner().should("not.exist"); + + // trigger refresh + dataJobExecutionsPage.waitForActionThinkingTime(); + dataJobExecutionsPage.refreshExecData(); + + // verify while refreshing + dataJobExecutionsPage.getDataGrid().should("exist"); + dataJobExecutionsPage.getExecLoadingSpinner().should("exist"); + dataJobExecutionsPage.getDataGridSpinner().should("exist"); + + // wait loading data to finish + dataJobExecutionsPage.waitForGridDataLoad(); + + // verify after refresh + dataJobExecutionsPage.getDataGrid().should("exist"); + dataJobExecutionsPage.getExecLoadingSpinner().should("not.exist"); + dataJobExecutionsPage.getDataGridSpinner().should("not.exist"); + }); + + describe("DataGrid Filters", () => { + it("should verify status filter options are rendered and behaves correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + // open status filter + dataJobExecutionsPage.openStatusFilter(); + + // verify exist + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + dataJobExecutionsPage + .getDataGridExecStatusFilters() + .then((elements) => Array.from(elements).map((el) => el.innerText)) + .should("deep.equal", [ + "Success", + "Platform Error", + "User Error", + "Running", + "Submitted", + "Skipped", + "Canceled", + ]); + + // close status filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - dataJobExecutionsPage.getDataGrid().should('exist'); + // open status filter and select all available values and close the filter + dataJobExecutionsPage.openStatusFilter(); + dataJobExecutionsPage.filterByStatus("user_error"); + dataJobExecutionsPage.filterByStatus("platform_error"); + dataJobExecutionsPage.filterByStatus("succeeded"); + dataJobExecutionsPage.filterByStatus("running"); + dataJobExecutionsPage.filterByStatus("skipped"); + dataJobExecutionsPage.filterByStatus("submitted"); + dataJobExecutionsPage.filterByStatus("cancelled"); + dataJobExecutionsPage.closeFilter(); + + // verify current URL has appended all execution statuses and sort by status ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: + '{"status":"user_error,platform_error,succeeded,running,skipped,submitted,cancelled"}', + }, + }); - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); + // open status filter and unselect statuses: platform_error, skipped, submitted, cancelled and close the filter + dataJobExecutionsPage.openStatusFilter(); + dataJobExecutionsPage.clearFilterByStatus("platform_error"); + dataJobExecutionsPage.clearFilterByStatus("skipped"); + dataJobExecutionsPage.clearFilterByStatus("submitted"); + dataJobExecutionsPage.clearFilterByStatus("cancelled"); + dataJobExecutionsPage.closeFilter(); + + // verify current URL has appended execution statuses user_error, succeeded, running and sort by status ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"status":"user_error,succeeded,running"}', + }, + }); + + // open status filter and unselect left statuses and close the filter + dataJobExecutionsPage.openStatusFilter(); + dataJobExecutionsPage.clearFilterByStatus("user_error"); + dataJobExecutionsPage.clearFilterByStatus("succeeded"); + dataJobExecutionsPage.clearFilterByStatus("running"); + dataJobExecutionsPage.closeFilter(); + + // verify current URL has appended only sort by status descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); }); - it('should verify start then cancel execution works', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(shortLivedTestJobWithDeployFixture.team, shortLivedTestJobWithDeployFixture.job_name); + it("should verify type filter options are rendered and behaves correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + // open type filter + dataJobExecutionsPage.openTypeFilter(); + + // verify exist + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + dataJobExecutionsPage + .getDataGridExecTypeFilterLabel("manual") + .should("exist") + .should("have.text", "Manual"); + dataJobExecutionsPage + .getDataGridExecTypeFilterLabel("scheduled") + .should("exist") + .should("have.text", "Scheduled"); + + // close type filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - DataJobManageExecutionsPage.waitForDataJobToNotHaveRunningExecution(); + // open type filter and select manual execution trigger and close + dataJobExecutionsPage.openTypeFilter(); + dataJobExecutionsPage.filterByType("manual"); + dataJobExecutionsPage.closeFilter(); + + // verify cell elements + dataJobExecutionsPage + .getDataGridExecTypeContainers("scheduled") + .should("have.length", 0); + + // verify current URL has appended manual execution trigger and sort by type ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"type":"manual"}', + }, + }); + + // open type filter and select scheduled execution trigger and close + dataJobExecutionsPage.openTypeFilter(); + dataJobExecutionsPage.filterByType("scheduled"); + dataJobExecutionsPage.closeFilter(); + + // verify cell elements + dataJobExecutionsPage + .getDataGridExecTypeContainers("manual") + .should("have.length.gte", 0); + dataJobExecutionsPage + .getDataGridExecTypeContainers("scheduled") + .should("have.length.gte", 0); + + // verify current URL has appended manual and scheduled execution trigger and sort by type ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"type":"manual,scheduled"}', + }, + }); + + // open type filter and unselect scheduled and manual execution triggers and close filter + dataJobExecutionsPage.openTypeFilter(); + dataJobExecutionsPage.clearFilterByType("scheduled"); + dataJobExecutionsPage.clearFilterByType("manual"); + dataJobExecutionsPage.closeFilter(); + + // verify cell elements + dataJobExecutionsPage + .getDataGridExecTypeContainers("manual") + .should("have.length.gte", 0); + dataJobExecutionsPage + .getDataGridExecTypeContainers("scheduled") + .should("have.length.gte", 0); + + // verify current URL has appended only sort by type descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + }); - dataJobExecutionsPage.getExecutionsTab().should('exist').should('have.class', 'active'); + it("should verify duration filter render input and filters correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - // Execute data job and check if the execution status after that is Running or Submitted - dataJobExecutionsPage.executeNow(true); - dataJobExecutionsPage - .getExecutionStatus() - .first() - .contains(/Running|Submitted/); + // open duration filter + dataJobExecutionsPage.openDurationFilter(); + + // verify exist fill with data na verify again + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + dataJobExecutionsPage.typeToTextFilterInput("100s"); + dataJobExecutionsPage.getDataGridRows().should("have.length", 0); + + // verify current URL has appended default sort by startTime descending and new value for id xyxyxy + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"duration":"100s"}', + }, + }); - // Cancel data job execution and check if the status after that is Cancelled - dataJobExecutionsPage.cancelExecution(true); - dataJobExecutionsPage.getExecutionStatus().first().should('contains.text', 'Canceled'); + // clear filter and verify + dataJobExecutionsPage.clearTextFilterInput(); + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // close id filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); }); - }); - describe('extended', () => { - it('should verify on URL navigate to Executions will open the page', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); + it("should verify execution start time filter render input and filters correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + + // open exec start time filter + dataJobExecutionsPage.openExecStartFilter(); + + // verify exist fill with data na verify again + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + dataJobExecutionsPage + .generateExecStartFilterValue() + .then((filterValue) => { + // type generated filter value + dataJobExecutionsPage.typeToTextFilterInput(filterValue); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecStartCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`${filterValue}$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); - dataJobExecutionsPage + // verify current URL has appended default sort by startTime descending and new value for startTime ${filterValue} + dataJobExecutionsPage .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: `{"startTime":"${filterValue}"}`, + }, }); + }); - dataJobExecutionsPage.getDetailsTab().should('exist').should('not.have.class', 'active'); + // clear filter and verify + dataJobExecutionsPage.clearTextFilterInput(); + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // close start time filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + }); - dataJobExecutionsPage.getExecutionsTab().should('exist').should('have.class', 'active'); + it("should verify execution end time filter render input and filters correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - dataJobExecutionsPage.getDataGrid().should('exist'); - }); + // open exec end time filter + dataJobExecutionsPage.openExecEndFilter(); + + // verify exist fill with data na verify again + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + dataJobExecutionsPage + .generateExecEndFilterValue() + .then((filterValue) => { + // type generated filter value + dataJobExecutionsPage.typeToTextFilterInput(filterValue); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecEndCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`${filterValue}$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); - it('should verify time period is in correct format', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); + // verify current URL has appended default sort by startTime descending and new value for endTime ${filterValue} + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: `{"endTime":"${filterValue}"}`, + }, + }); + }); - dataJobExecutionsPage.getTimePeriod().invoke('text').invoke('trim').should('match', new RegExp(`^\\w+\\s\\d+,\\s\\d+,\\s\\d+:\\d+:\\d+\\s(AM|PM)\\sto\\s\\w+\\s\\d+,\\s\\d+,\\s\\d+:\\d+:\\d+\\s(AM|PM)$`)); + // clear filter and verify + dataJobExecutionsPage.clearTextFilterInput(); + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // close start time filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); }); - it('should verify refresh button will show spinner and then load data', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); + it("should verify execution id filter render input and filters correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - // verify before - dataJobExecutionsPage.getDataGrid().should('exist'); - dataJobExecutionsPage.getExecLoadingSpinner().should('not.exist'); - dataJobExecutionsPage.getDataGridSpinner().should('not.exist'); + // open exec ID filter + dataJobExecutionsPage.openIDFilter(); + + // verify exist fill with data na verify again + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + // type job name as filter value + dataJobExecutionsPage.typeToTextFilterInput( + longLivedFailingJobFixture.job_name, + ); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecIDCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`^${longLivedFailingJobFixture.job_name}-\d+$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + + // verify current URL has appended default sort by startTime descending and new value for endTime ${filterValue} + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: `{"id":"${longLivedFailingJobFixture.job_name}"}`, + }, + }); - // trigger refresh - dataJobExecutionsPage.waitForActionThinkingTime(); - dataJobExecutionsPage.refreshExecData(); + // clear filter and type random text then verify + dataJobExecutionsPage.clearTextFilterInput(); + dataJobExecutionsPage.typeToTextFilterInput("xyxyxy"); + dataJobExecutionsPage.getDataGridRows().should("have.length", 0); + + // verify current URL has appended default sort by startTime descending and new value for id xyxyxy + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"id":"xyxyxy"}', + }, + }); + + // clear filter and verify + dataJobExecutionsPage.clearTextFilterInput(); + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // close start time filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + }); - // verify while refreshing - dataJobExecutionsPage.getDataGrid().should('exist'); - dataJobExecutionsPage.getExecLoadingSpinner().should('exist'); - dataJobExecutionsPage.getDataGridSpinner().should('exist'); + it("should verify version filter render input and filters correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - // wait loading data to finish - dataJobExecutionsPage.waitForGridDataLoad(); + // open version filter + dataJobExecutionsPage.openVersionFilter(); + + // verify exist fill with data na verify again + dataJobExecutionsPage.getDataGridPopupFilter().should("exist"); + dataJobExecutionsPage.typeToTextFilterInput("xyxyxy"); + dataJobExecutionsPage.getDataGridRows().should("have.length", 0); + + // verify current URL has appended default sort by startTime descending and new value for jobVersion xyxyxy + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"jobVersion":"xyxyxy"}', + }, + }); - // verify after refresh - dataJobExecutionsPage.getDataGrid().should('exist'); - dataJobExecutionsPage.getExecLoadingSpinner().should('not.exist'); - dataJobExecutionsPage.getDataGridSpinner().should('not.exist'); + // clear filter and verify + dataJobExecutionsPage.clearTextFilterInput(); + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // close version filter + dataJobExecutionsPage.closeFilter(); + + // verify doesn't exist + dataJobExecutionsPage.getDataGridPopupFilter().should("not.exist"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); }); + }); + + describe("DataGrid Sort", () => { + it("should verify status sort behaves correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecStatusHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "none"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - describe('DataGrid Filters', () => { - it('should verify status filter options are rendered and behaves correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - // open status filter - dataJobExecutionsPage.openStatusFilter(); - - // verify exist - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - dataJobExecutionsPage - .getDataGridExecStatusFilters() - .then((elements) => Array.from(elements).map((el) => el.innerText)) - .should('deep.equal', ['Success', 'Platform Error', 'User Error', 'Running', 'Submitted', 'Skipped', 'Canceled']); - - // close status filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open status filter and select all available values and close the filter - dataJobExecutionsPage.openStatusFilter(); - dataJobExecutionsPage.filterByStatus('user_error'); - dataJobExecutionsPage.filterByStatus('platform_error'); - dataJobExecutionsPage.filterByStatus('succeeded'); - dataJobExecutionsPage.filterByStatus('running'); - dataJobExecutionsPage.filterByStatus('skipped'); - dataJobExecutionsPage.filterByStatus('submitted'); - dataJobExecutionsPage.filterByStatus('cancelled'); - dataJobExecutionsPage.closeFilter(); - - // verify current URL has appended all execution statuses and sort by status ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"status":"user_error,platform_error,succeeded,running,skipped,submitted,cancelled"}' - } - }); - - // open status filter and unselect statuses: platform_error, skipped, submitted, cancelled and close the filter - dataJobExecutionsPage.openStatusFilter(); - dataJobExecutionsPage.clearFilterByStatus('platform_error'); - dataJobExecutionsPage.clearFilterByStatus('skipped'); - dataJobExecutionsPage.clearFilterByStatus('submitted'); - dataJobExecutionsPage.clearFilterByStatus('cancelled'); - dataJobExecutionsPage.closeFilter(); - - // verify current URL has appended execution statuses user_error, succeeded, running and sort by status ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"status":"user_error,succeeded,running"}' - } - }); - - // open status filter and unselect left statuses and close the filter - dataJobExecutionsPage.openStatusFilter(); - dataJobExecutionsPage.clearFilterByStatus('user_error'); - dataJobExecutionsPage.clearFilterByStatus('succeeded'); - dataJobExecutionsPage.clearFilterByStatus('running'); - dataJobExecutionsPage.closeFilter(); - - // verify current URL has appended only sort by status descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + // sort by status ascending + dataJobExecutionsPage.sortByExecStatus(); + + // verify sort by status is ascending + dataJobExecutionsPage + .getDataGridExecStatusHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended sort by status ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"status":1}', + }, }); - it('should verify type filter options are rendered and behaves correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - // open type filter - dataJobExecutionsPage.openTypeFilter(); - - // verify exist - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - dataJobExecutionsPage.getDataGridExecTypeFilterLabel('manual').should('exist').should('have.text', 'Manual'); - dataJobExecutionsPage.getDataGridExecTypeFilterLabel('scheduled').should('exist').should('have.text', 'Scheduled'); - - // close type filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open type filter and select manual execution trigger and close - dataJobExecutionsPage.openTypeFilter(); - dataJobExecutionsPage.filterByType('manual'); - dataJobExecutionsPage.closeFilter(); - - // verify cell elements - dataJobExecutionsPage.getDataGridExecTypeContainers('scheduled').should('have.length', 0); - - // verify current URL has appended manual execution trigger and sort by type ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"type":"manual"}' - } - }); - - // open type filter and select scheduled execution trigger and close - dataJobExecutionsPage.openTypeFilter(); - dataJobExecutionsPage.filterByType('scheduled'); - dataJobExecutionsPage.closeFilter(); - - // verify cell elements - dataJobExecutionsPage.getDataGridExecTypeContainers('manual').should('have.length.gte', 0); - dataJobExecutionsPage.getDataGridExecTypeContainers('scheduled').should('have.length.gte', 0); - - // verify current URL has appended manual and scheduled execution trigger and sort by type ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"type":"manual,scheduled"}' - } - }); - - // open type filter and unselect scheduled and manual execution triggers and close filter - dataJobExecutionsPage.openTypeFilter(); - dataJobExecutionsPage.clearFilterByType('scheduled'); - dataJobExecutionsPage.clearFilterByType('manual'); - dataJobExecutionsPage.closeFilter(); - - // verify cell elements - dataJobExecutionsPage.getDataGridExecTypeContainers('manual').should('have.length.gte', 0); - dataJobExecutionsPage.getDataGridExecTypeContainers('scheduled').should('have.length.gte', 0); - - // verify current URL has appended only sort by type descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + // sort by status descending + dataJobExecutionsPage.sortByExecStatus(); + + // verify sort by status is descending + dataJobExecutionsPage + .getDataGridExecStatusHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended sort by status descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"status":-1}', + }, }); + }); - it('should verify duration filter render input and filters correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open duration filter - dataJobExecutionsPage.openDurationFilter(); - - // verify exist fill with data na verify again - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - dataJobExecutionsPage.typeToTextFilterInput('100s'); - dataJobExecutionsPage.getDataGridRows().should('have.length', 0); - - // verify current URL has appended default sort by startTime descending and new value for id xyxyxy - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"duration":"100s"}' - } - }); - - // clear filter and verify - dataJobExecutionsPage.clearTextFilterInput(); - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // close id filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + it("should verify type sort behaves correctly", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecTypeHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "none"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, }); - it('should verify execution start time filter render input and filters correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open exec start time filter - dataJobExecutionsPage.openExecStartFilter(); - - // verify exist fill with data na verify again - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - dataJobExecutionsPage.generateExecStartFilterValue().then((filterValue) => { - // type generated filter value - dataJobExecutionsPage.typeToTextFilterInput(filterValue); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecStartCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`${filterValue}$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - - // verify current URL has appended default sort by startTime descending and new value for startTime ${filterValue} - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: `{"startTime":"${filterValue}"}` - } - }); - }); + // sort by type ascending + dataJobExecutionsPage.sortByExecType(); + + // verify sort is ascending + dataJobExecutionsPage + .getDataGridExecTypeHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended sort by type ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"type":1}', + }, + }); - // clear filter and verify - dataJobExecutionsPage.clearTextFilterInput(); - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // close start time filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + // sort by type descending + dataJobExecutionsPage.sortByExecType(); + + // verify sort is descending + dataJobExecutionsPage + .getDataGridExecTypeHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + // verify current URL has appended sort by type descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"type":-1}', + }, }); + }); - it('should verify execution end time filter render input and filters correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open exec end time filter - dataJobExecutionsPage.openExecEndFilter(); - - // verify exist fill with data na verify again - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - dataJobExecutionsPage.generateExecEndFilterValue().then((filterValue) => { - // type generated filter value - dataJobExecutionsPage.typeToTextFilterInput(filterValue); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecEndCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`${filterValue}$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - - // verify current URL has appended default sort by startTime descending and new value for endTime ${filterValue} - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: `{"endTime":"${filterValue}"}` - } - }); + it("should verify duration sort works", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecDurationHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "none"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + + // sort by duration ascending + dataJobExecutionsPage.sortByExecDuration(); + + dataJobExecutionsPage + .getDataGridExecDurationHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecDurationCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecDurationCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return ( + dataJobExecutionsPage.convertStringContentToSeconds( + elText1, + ) - + dataJobExecutionsPage.convertStringContentToSeconds(elText2) + ); }); + }) + .should("be.lte", 0); + + // verify current URL has appended sort by duration ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"duration":1}', + }, + }); - // clear filter and verify - dataJobExecutionsPage.clearTextFilterInput(); - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // close start time filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + // sort by duration descending + dataJobExecutionsPage.sortByExecDuration(); + + dataJobExecutionsPage + .getDataGridExecDurationHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecDurationCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecDurationCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return ( + dataJobExecutionsPage.convertStringContentToSeconds( + elText1, + ) - + dataJobExecutionsPage.convertStringContentToSeconds(elText2) + ); + }); + }) + .should("be.gte", 0); + + // verify current URL has appended sort by duration descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"duration":-1}', + }, }); + }); - it('should verify execution id filter render input and filters correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open exec ID filter - dataJobExecutionsPage.openIDFilter(); - - // verify exist fill with data na verify again - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - // type job name as filter value - dataJobExecutionsPage.typeToTextFilterInput(longLivedFailingJobFixture.job_name); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecIDCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`^${longLivedFailingJobFixture.job_name}-\d+$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - - // verify current URL has appended default sort by startTime descending and new value for endTime ${filterValue} - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: `{"id":"${longLivedFailingJobFixture.job_name}"}` - } - }); - - // clear filter and type random text then verify - dataJobExecutionsPage.clearTextFilterInput(); - dataJobExecutionsPage.typeToTextFilterInput('xyxyxy'); - dataJobExecutionsPage.getDataGridRows().should('have.length', 0); - - // verify current URL has appended default sort by startTime descending and new value for id xyxyxy - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"id":"xyxyxy"}' - } - }); - - // clear filter and verify - dataJobExecutionsPage.clearTextFilterInput(); - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // close start time filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + it("should verify execution start time sort works", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecStartHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage + .getDataGridExecStartCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecStartCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return new Date(elText1) - new Date(elText2); + }); + }) + .should("be.gte", 0); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, }); - it('should verify version filter render input and filters correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open version filter - dataJobExecutionsPage.openVersionFilter(); - - // verify exist fill with data na verify again - dataJobExecutionsPage.getDataGridPopupFilter().should('exist'); - dataJobExecutionsPage.typeToTextFilterInput('xyxyxy'); - dataJobExecutionsPage.getDataGridRows().should('have.length', 0); - - // verify current URL has appended default sort by startTime descending and new value for jobVersion xyxyxy - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"jobVersion":"xyxyxy"}' - } - }); - - // clear filter and verify - dataJobExecutionsPage.clearTextFilterInput(); - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // close version filter - dataJobExecutionsPage.closeFilter(); - - // verify doesn't exist - dataJobExecutionsPage.getDataGridPopupFilter().should('not.exist'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); + // sort by exec start ascending + dataJobExecutionsPage.sortByExecStart(); + + dataJobExecutionsPage + .getDataGridExecStartHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecStartCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecStartCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return new Date(elText1) - new Date(elText2); + }); + }) + .should("be.lte", 0); + + // verify current URL has appended sort by duration ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":1}', + }, }); }); - describe('DataGrid Sort', () => { - it('should verify status sort behaves correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecStatusHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'none'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by status ascending - dataJobExecutionsPage.sortByExecStatus(); - - // verify sort by status is ascending - dataJobExecutionsPage.getDataGridExecStatusHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended sort by status ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"status":1}' - } - }); - - // sort by status descending - dataJobExecutionsPage.sortByExecStatus(); - - // verify sort by status is descending - dataJobExecutionsPage.getDataGridExecStatusHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended sort by status descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"status":-1}' - } - }); + it("should verify execution end time sort works", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecEndHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "none"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, }); - it('should verify type sort behaves correctly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecTypeHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'none'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by type ascending - dataJobExecutionsPage.sortByExecType(); - - // verify sort is ascending - dataJobExecutionsPage.getDataGridExecTypeHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended sort by type ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"type":1}' - } - }); - - // sort by type descending - dataJobExecutionsPage.sortByExecType(); - - // verify sort is descending - dataJobExecutionsPage.getDataGridExecTypeHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - // verify current URL has appended sort by type descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"type":-1}' - } - }); + // sort by exec end ascending + dataJobExecutionsPage.sortByExecEnd(); + + dataJobExecutionsPage + .getDataGridExecEndHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecEndCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecEndCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + const d1 = elText1 ? new Date(elText1) : new Date(); + const d2 = elText2 ? new Date(elText2) : new Date(); + + return d1 - d2; + }); + }) + .should("be.lte", 0); + + // verify current URL has appended sort by endTime ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"endTime":1}', + }, }); - it('should verify duration sort works', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecDurationHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'none'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by duration ascending - dataJobExecutionsPage.sortByExecDuration(); - - dataJobExecutionsPage.getDataGridExecDurationHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecDurationCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecDurationCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return dataJobExecutionsPage.convertStringContentToSeconds(elText1) - dataJobExecutionsPage.convertStringContentToSeconds(elText2); - }); - }) - .should('be.lte', 0); - - // verify current URL has appended sort by duration ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"duration":1}' - } - }); - - // sort by duration descending - dataJobExecutionsPage.sortByExecDuration(); - - dataJobExecutionsPage.getDataGridExecDurationHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecDurationCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecDurationCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return dataJobExecutionsPage.convertStringContentToSeconds(elText1) - dataJobExecutionsPage.convertStringContentToSeconds(elText2); - }); - }) - .should('be.gte', 0); - - // verify current URL has appended sort by duration descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"duration":-1}' - } - }); + // sort by exec end time descending + dataJobExecutionsPage.sortByExecEnd(); + + dataJobExecutionsPage + .getDataGridExecEndHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecEndCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecEndCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + const d1 = elText1 ? new Date(elText1) : new Date(); + const d2 = elText2 ? new Date(elText2) : new Date(); + + return d1 - d2; + }); + }) + .should("be.gte", 0); + + // verify current URL has appended sort by endTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"endTime":-1}', + }, }); + }); - it('should verify execution start time sort works', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecStartHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage - .getDataGridExecStartCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecStartCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return new Date(elText1) - new Date(elText2); - }); - }) - .should('be.gte', 0); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by exec start ascending - dataJobExecutionsPage.sortByExecStart(); - - dataJobExecutionsPage.getDataGridExecStartHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecStartCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecStartCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return new Date(elText1) - new Date(elText2); - }); - }) - .should('be.lte', 0); - - // verify current URL has appended sort by duration ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":1}' - } - }); + it("should verify execution ID sort works", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecIDHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "none"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, }); - it('should verify execution end time sort works', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecEndHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'none'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by exec end ascending - dataJobExecutionsPage.sortByExecEnd(); - - dataJobExecutionsPage.getDataGridExecEndHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecEndCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecEndCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - const d1 = elText1 ? new Date(elText1) : new Date(); - const d2 = elText2 ? new Date(elText2) : new Date(); - - return d1 - d2; - }); - }) - .should('be.lte', 0); - - // verify current URL has appended sort by endTime ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"endTime":1}' - } - }); - - // sort by exec end time descending - dataJobExecutionsPage.sortByExecEnd(); - - dataJobExecutionsPage.getDataGridExecEndHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecEndCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecEndCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - const d1 = elText1 ? new Date(elText1) : new Date(); - const d2 = elText2 ? new Date(elText2) : new Date(); - - return d1 - d2; - }); - }) - .should('be.gte', 0); - - // verify current URL has appended sort by endTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"endTime":-1}' - } - }); + // sort by ID ascending + dataJobExecutionsPage.sortByExecID(); + + dataJobExecutionsPage + .getDataGridExecIDHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecIDCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecIDCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return elText2 > elText1; + }); + }) + .should("be.true"); + + // verify current URL has appended sort by id ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"id":1}', + }, }); - it('should verify execution ID sort works', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecIDHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'none'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by ID ascending - dataJobExecutionsPage.sortByExecID(); - - dataJobExecutionsPage.getDataGridExecIDHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecIDCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecIDCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return elText2 > elText1; - }); - }) - .should('be.true'); - - // verify current URL has appended sort by id ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"id":1}' - } - }); - - // sort by ID descending - dataJobExecutionsPage.sortByExecID(); - - dataJobExecutionsPage.getDataGridExecIDHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecIDCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecIDCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return elText1 > elText2; - }); - }) - .should('be.true'); - - // verify current URL has appended sort by id descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"id":-1}' - } - }); + // sort by ID descending + dataJobExecutionsPage.sortByExecID(); + + dataJobExecutionsPage + .getDataGridExecIDHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecIDCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecIDCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return elText1 > elText2; + }); + }) + .should("be.true"); + + // verify current URL has appended sort by id descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"id":-1}', + }, }); + }); - it('should verify execution version sort works', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage.getDataGridExecVersionHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'none'); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // sort by version ascending - dataJobExecutionsPage.sortByExecVersion(); - - dataJobExecutionsPage.getDataGridExecVersionHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecVersionCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecVersionCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return elText2 >= elText1; - }); - }) - .should('be.true'); - - // verify current URL has appended sort by job version ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"jobVersion":1}' - } - }); - - // sort by version descending - dataJobExecutionsPage.sortByExecVersion(); - - dataJobExecutionsPage.getDataGridExecVersionHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'descending'); - - dataJobExecutionsPage.getDataGridRows().should('have.length.gt', 0); - - dataJobExecutionsPage - .getDataGridExecVersionCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecVersionCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return elText1 >= elText2; - }); - }) - .should('be.true'); - - // verify current URL has appended sort by job version descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"jobVersion":-1}' - } - }); + it("should verify execution version sort works", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecVersionHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "none"); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); + + // sort by version ascending + dataJobExecutionsPage.sortByExecVersion(); + + dataJobExecutionsPage + .getDataGridExecVersionHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecVersionCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecVersionCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return elText2 >= elText1; + }); + }) + .should("be.true"); + + // verify current URL has appended sort by job version ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"jobVersion":1}', + }, + }); + + // sort by version descending + dataJobExecutionsPage.sortByExecVersion(); + + dataJobExecutionsPage + .getDataGridExecVersionHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "descending"); + + dataJobExecutionsPage.getDataGridRows().should("have.length.gt", 0); + + dataJobExecutionsPage + .getDataGridExecVersionCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecVersionCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return elText1 >= elText2; + }); + }) + .should("be.true"); + + // verify current URL has appended sort by job version descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"jobVersion":-1}', + }, }); }); + }); + + // !!! important tests order is very important, because generated url from 1st test is used in the second test + // the second test cannot be run without previously running the first test + describe("DataGrid Filters and Sort to URL", () => { + // value is assigned at the end of the 1st test and used in the 2nd test + let navigationUrlWithFiltersAndSort = ""; + + it("should verify multiple filters and sort are appended to URL", () => { + const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo( + longLivedFailingJobFixture.team, + longLivedFailingJobFixture.job_name, + ); + + // verify current URL has appended default sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + }, + }); - // !!! important tests order is very important, because generated url from 1st test is used in the second test - // the second test cannot be run without previously running the first test - describe('DataGrid Filters and Sort to URL', () => { - // value is assigned at the end of the 1st test and used in the 2nd test - let navigationUrlWithFiltersAndSort = ''; - - it('should verify multiple filters and sort are appended to URL', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateTo(longLivedFailingJobFixture.team, longLivedFailingJobFixture.job_name); - - // verify current URL has appended default sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}' - } - }); - - // open status filter and select user_error and close the filter - dataJobExecutionsPage.openStatusFilter(); - dataJobExecutionsPage.filterByStatus('user_error'); - dataJobExecutionsPage.filterByStatus('platform_error'); - dataJobExecutionsPage.closeFilter(); - - // verify current URL has appended filters execution status: user_error and platform_error, and sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"status":"user_error,platform_error"}' - } - }); - - // open type filter and select manual execution then verify, then select scheduled trigger and close - dataJobExecutionsPage.openTypeFilter(); - dataJobExecutionsPage.filterByType('manual'); - dataJobExecutionsPage.getDataGridExecTypeContainers('scheduled').should('have.length', 0); - dataJobExecutionsPage.filterByType('scheduled'); - dataJobExecutionsPage.closeFilter(); - - // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, and sort by startTime descending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"startTime":-1}', - filter: '{"status":"user_error,platform_error","type":"manual,scheduled"}' - } - }); - - // sort by exec ID ascending - dataJobExecutionsPage.sortByExecID(); - - // open exec ID filter - dataJobExecutionsPage.openIDFilter(); - // type job name as filter value - dataJobExecutionsPage.typeToTextFilterInput(longLivedFailingJobFixture.job_name); - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecIDCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`^${longLivedFailingJobFixture.job_name}-\d+$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name and sort by id ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"id":1}', - filter: `{"status":"user_error,platform_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}"}` - } - }); - - // open exec start time filter and type generated filterValue and close - dataJobExecutionsPage.openExecStartFilter(); - dataJobExecutionsPage.generateExecStartFilterValue().then((filterValue) => { - // type generated filter value - dataJobExecutionsPage.typeToTextFilterInput(filterValue); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecStartCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`${filterValue}$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - - // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue} and sort by id ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"id":1}', - filter: `{"status":"user_error,platform_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}","startTime":"${filterValue}"}` - } - }); + // open status filter and select user_error and close the filter + dataJobExecutionsPage.openStatusFilter(); + dataJobExecutionsPage.filterByStatus("user_error"); + dataJobExecutionsPage.filterByStatus("platform_error"); + dataJobExecutionsPage.closeFilter(); + + // verify current URL has appended filters execution status: user_error and platform_error, and sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: '{"status":"user_error,platform_error"}', + }, + }); + + // open type filter and select manual execution then verify, then select scheduled trigger and close + dataJobExecutionsPage.openTypeFilter(); + dataJobExecutionsPage.filterByType("manual"); + dataJobExecutionsPage + .getDataGridExecTypeContainers("scheduled") + .should("have.length", 0); + dataJobExecutionsPage.filterByType("scheduled"); + dataJobExecutionsPage.closeFilter(); + + // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, and sort by startTime descending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"startTime":-1}', + filter: + '{"status":"user_error,platform_error","type":"manual,scheduled"}', + }, + }); + + // sort by exec ID ascending + dataJobExecutionsPage.sortByExecID(); + + // open exec ID filter + dataJobExecutionsPage.openIDFilter(); + // type job name as filter value + dataJobExecutionsPage.typeToTextFilterInput( + longLivedFailingJobFixture.job_name, + ); + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecIDCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`^${longLivedFailingJobFixture.job_name}-\d+$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name and sort by id ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"id":1}', + filter: `{"status":"user_error,platform_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}"}`, + }, + }); + + // open exec start time filter and type generated filterValue and close + dataJobExecutionsPage.openExecStartFilter(); + dataJobExecutionsPage + .generateExecStartFilterValue() + .then((filterValue) => { + // type generated filter value + dataJobExecutionsPage.typeToTextFilterInput(filterValue); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecStartCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`${filterValue}$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + + // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue} and sort by id ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"id":1}', + filter: `{"status":"user_error,platform_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}","startTime":"${filterValue}"}`, + }, }); - dataJobExecutionsPage.closeFilter(); - - // open exec end time filter and type generated filterValue and close - dataJobExecutionsPage.openExecEndFilter(); - dataJobExecutionsPage.generateExecEndFilterValue().then((filterValue) => { - // type generated filter value - dataJobExecutionsPage.typeToTextFilterInput(filterValue); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecEndCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`${filterValue}$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - - // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue}, endTime: ${filterValue} and sort by id ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"id":1}', - filter: `{"status":"user_error,platform_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}","startTime":"${filterValue}","endTime":"${filterValue}"}` - } - }); + }); + dataJobExecutionsPage.closeFilter(); + + // open exec end time filter and type generated filterValue and close + dataJobExecutionsPage.openExecEndFilter(); + dataJobExecutionsPage + .generateExecEndFilterValue() + .then((filterValue) => { + // type generated filter value + dataJobExecutionsPage.typeToTextFilterInput(filterValue); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecEndCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`${filterValue}$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + + // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue}, endTime: ${filterValue} and sort by id ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"id":1}', + filter: `{"status":"user_error,platform_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}","startTime":"${filterValue}","endTime":"${filterValue}"}`, + }, }); - dataJobExecutionsPage.closeFilter(); - - // extract current url for next test - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .then((urlNormalized) => { - const pathSegment = urlNormalized.pathSegment; - const queryParamsSerialized = Object.entries(urlNormalized.queryParams) - .map((keyValuePair) => keyValuePair.join('=')) - .join('&'); - navigationUrlWithFiltersAndSort = `${pathSegment}?${queryParamsSerialized}`; - - cy.log(navigationUrlWithFiltersAndSort); - console.log(navigationUrlWithFiltersAndSort); - }); }); + dataJobExecutionsPage.closeFilter(); + + // extract current url for next test + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .then((urlNormalized) => { + const pathSegment = urlNormalized.pathSegment; + const queryParamsSerialized = Object.entries( + urlNormalized.queryParams, + ) + .map((keyValuePair) => keyValuePair.join("=")) + .join("&"); + navigationUrlWithFiltersAndSort = `${pathSegment}?${queryParamsSerialized}`; + + cy.log(navigationUrlWithFiltersAndSort); + console.log(navigationUrlWithFiltersAndSort); + }); + }); + + it("should verify URL navigation with filters and sort will prefill everything and filter accordingly", () => { + const dataJobExecutionsPage = + DataJobManageExecutionsPage.navigateToExecutionsWithUrl( + navigationUrlWithFiltersAndSort, + ); - it('should verify URL navigation with filters and sort will prefill everything and filter accordingly', () => { - const dataJobExecutionsPage = DataJobManageExecutionsPage.navigateToExecutionsWithUrl(navigationUrlWithFiltersAndSort); - - // open status filter, verify then close - dataJobExecutionsPage.openStatusFilter(); - dataJobExecutionsPage.getDataGridExecStatusFilterCheckboxesStatuses().should('deep.equal', [ - ['succeeded', false], - ['platform_error', true], - ['user_error', true], - ['running', false], - ['submitted', false], - ['skipped', false], - ['cancelled', false] - ]); - dataJobExecutionsPage.closeFilter(); - - // open type filter, verify then close - dataJobExecutionsPage.openTypeFilter(); - dataJobExecutionsPage.getDataGridExecTypeFilterCheckboxesStatuses().should('deep.equal', [ - ['manual', true], - ['scheduled', true] - ]); - dataJobExecutionsPage.getDataGridExecTypeContainers('manual').should('have.length.gte', 0); - dataJobExecutionsPage.getDataGridExecTypeContainers('scheduled').should('have.length.gte', 0); - dataJobExecutionsPage.closeFilter(); - - // verify sort by id ascending - dataJobExecutionsPage.getDataGridExecIDHeader().should('exist').invoke('attr', 'aria-sort').should('eq', 'ascending'); - dataJobExecutionsPage - .getDataGridExecIDCell(1) - .invoke('text') - .invoke('trim') - .then((elText1) => { - return dataJobExecutionsPage - .getDataGridExecIDCell(2) - .invoke('text') - .invoke('trim') - .then((elText2) => { - return elText2 > elText1; - }); - }) - .should('be.true'); - - // open id filter, verify then close - dataJobExecutionsPage.openIDFilter(); - dataJobExecutionsPage.getDataGridInputFilter().should('exist').should('have.value', longLivedFailingJobFixture.job_name); - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecIDCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`^${longLivedFailingJobFixture.job_name}-\d+$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - dataJobExecutionsPage.closeFilter(); - - // open exec start time filter, verify then close - dataJobExecutionsPage.openExecStartFilter(); - dataJobExecutionsPage.generateExecStartFilterValue().then((filterValue) => { - // verify generated value prefilled in input field - dataJobExecutionsPage.getDataGridInputFilter().should('exist').should('have.value', filterValue); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecStartCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`${filterValue}$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); + // open status filter, verify then close + dataJobExecutionsPage.openStatusFilter(); + dataJobExecutionsPage + .getDataGridExecStatusFilterCheckboxesStatuses() + .should("deep.equal", [ + ["succeeded", false], + ["platform_error", true], + ["user_error", true], + ["running", false], + ["submitted", false], + ["skipped", false], + ["cancelled", false], + ]); + dataJobExecutionsPage.closeFilter(); + + // open type filter, verify then close + dataJobExecutionsPage.openTypeFilter(); + dataJobExecutionsPage + .getDataGridExecTypeFilterCheckboxesStatuses() + .should("deep.equal", [ + ["manual", true], + ["scheduled", true], + ]); + dataJobExecutionsPage + .getDataGridExecTypeContainers("manual") + .should("have.length.gte", 0); + dataJobExecutionsPage + .getDataGridExecTypeContainers("scheduled") + .should("have.length.gte", 0); + dataJobExecutionsPage.closeFilter(); + + // verify sort by id ascending + dataJobExecutionsPage + .getDataGridExecIDHeader() + .should("exist") + .invoke("attr", "aria-sort") + .should("eq", "ascending"); + dataJobExecutionsPage + .getDataGridExecIDCell(1) + .invoke("text") + .invoke("trim") + .then((elText1) => { + return dataJobExecutionsPage + .getDataGridExecIDCell(2) + .invoke("text") + .invoke("trim") + .then((elText2) => { + return elText2 > elText1; }); - dataJobExecutionsPage.closeFilter(); - - // open exec end time filter, verify then close - dataJobExecutionsPage.openExecEndFilter(); - dataJobExecutionsPage.generateExecEndFilterValue().then((filterValue) => { - // verify generated value prefilled in input field - dataJobExecutionsPage.getDataGridInputFilter().should('exist').should('have.value', filterValue); - - // verify only cells that match the value are rendered - dataJobExecutionsPage - .getDataGridExecEndCells() - .then(($cells) => { - const foundCells = Array.from($cells).map((cell) => new RegExp(`${filterValue}$`).test(`${cell.innerText?.trim()}`)); - - return foundCells.length; - }) - .should('gt', 0); - - // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue}, endTime: ${filterValue} and sort by id ascending - dataJobExecutionsPage - .getCurrentUrlNormalized({ - includePathSegment: true, - includeQueryString: true, - decodeQueryString: true - }) - .should('deep.equal', { - pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, - queryParams: { - sort: '{"id":1}', - filter: `{"status":"user_error,platform_error","type":"manual,scheduled","startTime":"${filterValue}","endTime":"${filterValue}","id":"${longLivedFailingJobFixture.job_name}"}` - } - }); + }) + .should("be.true"); + + // open id filter, verify then close + dataJobExecutionsPage.openIDFilter(); + dataJobExecutionsPage + .getDataGridInputFilter() + .should("exist") + .should("have.value", longLivedFailingJobFixture.job_name); + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecIDCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`^${longLivedFailingJobFixture.job_name}-\d+$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + dataJobExecutionsPage.closeFilter(); + + // open exec start time filter, verify then close + dataJobExecutionsPage.openExecStartFilter(); + dataJobExecutionsPage + .generateExecStartFilterValue() + .then((filterValue) => { + // verify generated value prefilled in input field + dataJobExecutionsPage + .getDataGridInputFilter() + .should("exist") + .should("have.value", filterValue); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecStartCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`${filterValue}$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + }); + dataJobExecutionsPage.closeFilter(); + + // open exec end time filter, verify then close + dataJobExecutionsPage.openExecEndFilter(); + dataJobExecutionsPage + .generateExecEndFilterValue() + .then((filterValue) => { + // verify generated value prefilled in input field + dataJobExecutionsPage + .getDataGridInputFilter() + .should("exist") + .should("have.value", filterValue); + + // verify only cells that match the value are rendered + dataJobExecutionsPage + .getDataGridExecEndCells() + .then(($cells) => { + const foundCells = Array.from($cells).map((cell) => + new RegExp(`${filterValue}$`).test( + `${cell.innerText?.trim()}`, + ), + ); + + return foundCells.length; + }) + .should("gt", 0); + + // verify current URL has appended filters execution status: user_error and platform_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue}, endTime: ${filterValue} and sort by id ascending + dataJobExecutionsPage + .getCurrentUrlNormalized({ + includePathSegment: true, + includeQueryString: true, + decodeQueryString: true, + }) + .should("deep.equal", { + pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, + queryParams: { + sort: '{"id":1}', + filter: `{"status":"user_error,platform_error","type":"manual,scheduled","startTime":"${filterValue}","endTime":"${filterValue}","id":"${longLivedFailingJobFixture.job_name}"}`, + }, }); - dataJobExecutionsPage.closeFilter(); }); + dataJobExecutionsPage.closeFilter(); }); + }); }); -}); + }, +); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/router/router.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/router/router.spec.js index 445f3e31d6..141735821d 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/router/router.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/frontend-tests/router/router.spec.js @@ -5,20 +5,22 @@ /// -import { BasePagePO } from '../../../support/pages/base/base-page.po'; +import { BasePagePO } from "../../../support/pages/base/base-page.po"; -describe('Routing for pages', () => { - describe('smoke', { tags: ['@smoke'] }, () => { - it('navigates to get-started page when explore page route is ignored', () => { - BasePagePO.executeCypressCommand('appConfigInterceptorDisableExploreRoute'); - // wait for login - BasePagePO.wireUserSession(); - BasePagePO.initInterceptors(); - BasePagePO.navigateTo(); - // go to explore page url - cy.visit('/explore/data-jobs'); - // should navigate to get-started instead - cy.location().should((l) => expect(l.pathname).to.equal('/get-started')); - }); +describe("Routing for pages", () => { + describe("smoke", { tags: ["@smoke"] }, () => { + it("navigates to get-started page when explore page route is ignored", () => { + BasePagePO.executeCypressCommand( + "appConfigInterceptorDisableExploreRoute", + ); + // wait for login + BasePagePO.wireUserSession(); + BasePagePO.initInterceptors(); + BasePagePO.navigateTo(); + // go to explore page url + cy.visit("/explore/data-jobs"); + // should navigate to get-started instead + cy.location().should((l) => expect(l.pathname).to.equal("/get-started")); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/quickstart-operability/quickstart.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/quickstart-operability/quickstart.spec.js index 17fa3e1247..5f70fef349 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/quickstart-operability/quickstart.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/quickstart-operability/quickstart.spec.js @@ -5,28 +5,42 @@ /// -import { GetStartedPagePO } from '../../support/pages/get-started/get-started-page.po'; -import { DataJobsManagePage } from '../../support/pages/manage/data-jobs/data-jobs.po'; -import { BasePagePO } from '../../support/pages/base/base-page.po'; +import { GetStartedPagePO } from "../../support/pages/get-started/get-started-page.po"; +import { DataJobsManagePage } from "../../support/pages/manage/data-jobs/data-jobs.po"; +import { BasePagePO } from "../../support/pages/base/base-page.po"; -describe('Check if quickstart-vdk frontend is operational', () => { - describe('smoke', { tags: ['@smoke'] }, () => { - it('navigates to root page and the page loads correctly', () => { - const getStartedPage = BasePagePO.navigateToNoBootstrap(); - getStartedPage.getPageTitle().invoke('text').invoke('trim').should('eq', 'Get Started with Data Pipelines'); - }); +describe("Check if quickstart-vdk frontend is operational", () => { + describe("smoke", { tags: ["@smoke"] }, () => { + it("navigates to root page and the page loads correctly", () => { + const getStartedPage = BasePagePO.navigateToNoBootstrap(); + getStartedPage + .getPageTitle() + .invoke("text") + .invoke("trim") + .should("eq", "Get Started with Data Pipelines"); + }); - it('navigates to get-started page and the page loads correctly', () => { - const getStartedPage = GetStartedPagePO.navigateToNoBootstrap(); - getStartedPage.getPageTitle().invoke('text').invoke('trim').should('eq', 'Get Started with Data Pipelines'); - }); + it("navigates to get-started page and the page loads correctly", () => { + const getStartedPage = GetStartedPagePO.navigateToNoBootstrap(); + getStartedPage + .getPageTitle() + .invoke("text") + .invoke("trim") + .should("eq", "Get Started with Data Pipelines"); + }); - it('navigates to manage page and the page loads correctly', () => { - const dataJobsManagePage = DataJobsManagePage.navigateToNoBootstrap(); + it("navigates to manage page and the page loads correctly", () => { + const dataJobsManagePage = DataJobsManagePage.navigateToNoBootstrap(); - dataJobsManagePage.getPageTitle().scrollIntoView().should('be.visible').invoke('text').invoke('trim').should('eq', 'Manage Data Jobs'); + dataJobsManagePage + .getPageTitle() + .scrollIntoView() + .should("be.visible") + .invoke("text") + .invoke("trim") + .should("eq", "Manage Data Jobs"); - dataJobsManagePage.getDataGrid().scrollIntoView().should('be.visible'); - }); + dataJobsManagePage.getDataGrid().scrollIntoView().should("be.visible"); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/authentication-helpers.plugins.js b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/authentication-helpers.plugins.js index 791298fd5c..4938765cec 100644 --- a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/authentication-helpers.plugins.js +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/authentication-helpers.plugins.js @@ -6,28 +6,28 @@ let ACCESS_TOKEN; const getAccessTokenSynchronous = () => { - return ACCESS_TOKEN; + return ACCESS_TOKEN; }; const setAccessTokenSynchronous = (token) => { - ACCESS_TOKEN = token; + ACCESS_TOKEN = token; - return ACCESS_TOKEN; + return ACCESS_TOKEN; }; const getAccessTokenAsynchronous = () => { - return Promise.resolve(ACCESS_TOKEN); + return Promise.resolve(ACCESS_TOKEN); }; const setAccessTokenAsynchronous = (token) => { - ACCESS_TOKEN = token; + ACCESS_TOKEN = token; - return Promise.resolve(ACCESS_TOKEN); + return Promise.resolve(ACCESS_TOKEN); }; module.exports = { - getAccessTokenSynchronous, - setAccessTokenSynchronous, - getAccessTokenAsynchronous, - setAccessTokenAsynchronous + getAccessTokenSynchronous, + setAccessTokenSynchronous, + getAccessTokenAsynchronous, + setAccessTokenAsynchronous, }; diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/http-helpers.plugins.js b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/http-helpers.plugins.js index d1b18d1835..b0c4c3905c 100644 --- a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/http-helpers.plugins.js +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/http-helpers.plugins.js @@ -3,15 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -const { default: axios } = require('axios'); +const { default: axios } = require("axios"); -const { cloneDeep } = require('lodash'); +const { cloneDeep } = require("lodash"); -const { Logger } = require('./logger-helpers.plugins'); +const { Logger } = require("./logger-helpers.plugins"); -const { trimArraysToNElements } = require('./util-helpers.plugins'); +const { trimArraysToNElements } = require("./util-helpers.plugins"); -const { getAccessTokenSynchronous } = require('./authentication-helpers.plugins'); +const { + getAccessTokenSynchronous, +} = require("./authentication-helpers.plugins"); /** * ** Create Http Get request. @@ -22,30 +24,37 @@ const { getAccessTokenSynchronous } = require('./authentication-helpers.plugins' * @returns {Promise>} */ const httpGetReq = (url, headers = {}, auth = undefined) => { - const startTime = new Date(); - - Logger.debug(`HTTP Get request for Url =>`, url); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return axios - .request({ - url, - method: 'get', - headers: { - authorization: `Bearer ${getAccessTokenSynchronous()}`, - ...headers - }, - validateStatus: () => true, - auth - }) - .then((response) => { - const endTime = new Date(); - - Logger.debug(`HTTP Get response from Url with Body`, url, _filterResponseDataForDebug(response)); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - - return _throttle(response); - }); + const startTime = new Date(); + + Logger.debug(`HTTP Get request for Url =>`, url); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return axios + .request({ + url, + method: "get", + headers: { + authorization: `Bearer ${getAccessTokenSynchronous()}`, + ...headers, + }, + validateStatus: () => true, + auth, + }) + .then((response) => { + const endTime = new Date(); + + Logger.debug( + `HTTP Get response from Url with Body`, + url, + _filterResponseDataForDebug(response), + ); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + + return _throttle(response); + }); }; /** @@ -57,30 +66,37 @@ const httpGetReq = (url, headers = {}, auth = undefined) => { * @returns {Promise>} */ const httpPostReq = (url, body, headers = {}) => { - const startTime = new Date(); - - Logger.debug(`HTTP Post request to Url with Body =>`, url, body); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return axios - .request({ - url: url, - method: 'post', - headers: { - authorization: `Bearer ${getAccessTokenSynchronous()}`, - ...headers - }, - data: body, - validateStatus: () => true - }) - .then((response) => { - const endTime = new Date(); - - Logger.debug(`HTTP Post response from Url with Body`, url, _filterResponseDataForDebug(response)); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - - return _throttle(response); - }); + const startTime = new Date(); + + Logger.debug(`HTTP Post request to Url with Body =>`, url, body); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return axios + .request({ + url: url, + method: "post", + headers: { + authorization: `Bearer ${getAccessTokenSynchronous()}`, + ...headers, + }, + data: body, + validateStatus: () => true, + }) + .then((response) => { + const endTime = new Date(); + + Logger.debug( + `HTTP Post response from Url with Body`, + url, + _filterResponseDataForDebug(response), + ); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + + return _throttle(response); + }); }; /** @@ -92,30 +108,37 @@ const httpPostReq = (url, body, headers = {}) => { * @returns {Promise>} */ const httpPatchReq = (url, body, headers = {}) => { - const startTime = new Date(); - - Logger.debug(`HTTP Patch request to Url with Body =>`, url, body); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return axios - .request({ - url: url, - method: 'patch', - headers: { - authorization: `Bearer ${getAccessTokenSynchronous()}`, - ...headers - }, - data: body, - validateStatus: () => true - }) - .then((response) => { - const endTime = new Date(); - - Logger.debug(`HTTP Patch response from Url with Body`, url, _filterResponseDataForDebug(response)); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - - return _throttle(response); - }); + const startTime = new Date(); + + Logger.debug(`HTTP Patch request to Url with Body =>`, url, body); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return axios + .request({ + url: url, + method: "patch", + headers: { + authorization: `Bearer ${getAccessTokenSynchronous()}`, + ...headers, + }, + data: body, + validateStatus: () => true, + }) + .then((response) => { + const endTime = new Date(); + + Logger.debug( + `HTTP Patch response from Url with Body`, + url, + _filterResponseDataForDebug(response), + ); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + + return _throttle(response); + }); }; /** @@ -127,30 +150,37 @@ const httpPatchReq = (url, body, headers = {}) => { * @returns {Promise>} */ const httpPutReq = (url, body, headers = {}) => { - const startTime = new Date(); - - Logger.debug(`HTTP Put request to Url with Body =>`, url, body); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return axios - .request({ - url: url, - method: 'put', - headers: { - authorization: `Bearer ${getAccessTokenSynchronous()}`, - ...headers - }, - data: body, - validateStatus: () => true - }) - .then((response) => { - const endTime = new Date(); - - Logger.debug(`HTTP Put response from Url with Body`, url, _filterResponseDataForDebug(response)); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - - return _throttle(response); - }); + const startTime = new Date(); + + Logger.debug(`HTTP Put request to Url with Body =>`, url, body); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return axios + .request({ + url: url, + method: "put", + headers: { + authorization: `Bearer ${getAccessTokenSynchronous()}`, + ...headers, + }, + data: body, + validateStatus: () => true, + }) + .then((response) => { + const endTime = new Date(); + + Logger.debug( + `HTTP Put response from Url with Body`, + url, + _filterResponseDataForDebug(response), + ); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + + return _throttle(response); + }); }; /** @@ -161,29 +191,36 @@ const httpPutReq = (url, body, headers = {}) => { * @returns {Promise>} */ const httpDeleteReq = (url, headers = {}) => { - const startTime = new Date(); - - Logger.debug(`HTTP Delete request for Url =>`, url); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return axios - .request({ - url: url, - method: 'delete', - headers: { - authorization: `Bearer ${getAccessTokenSynchronous()}`, - ...headers - }, - validateStatus: () => true - }) - .then((response) => { - const endTime = new Date(); - - Logger.debug(`HTTP Delete response from Url with Body`, url, _filterResponseDataForDebug(response)); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - - return _throttle(response); - }); + const startTime = new Date(); + + Logger.debug(`HTTP Delete request for Url =>`, url); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return axios + .request({ + url: url, + method: "delete", + headers: { + authorization: `Bearer ${getAccessTokenSynchronous()}`, + ...headers, + }, + validateStatus: () => true, + }) + .then((response) => { + const endTime = new Date(); + + Logger.debug( + `HTTP Delete response from Url with Body`, + url, + _filterResponseDataForDebug(response), + ); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + + return _throttle(response); + }); }; /** @@ -194,25 +231,25 @@ const httpDeleteReq = (url, headers = {}) => { * @private */ const _filterResponseDataForDebug = (response) => { - return { - status: response.status, - statusText: response.statusText, - headers: { - ...response.headers, - authorization: '... removed from logs ...' - }, - config: { - timeout: response.config.timeout, - headers: { - ...response.config.headers, - authorization: '... removed from logs ...' - }, - url: response.config.url, - method: response.config.method, - data: response.config.data - }, - data: trimArraysToNElements(cloneDeep(response.data), 5) - }; + return { + status: response.status, + statusText: response.statusText, + headers: { + ...response.headers, + authorization: "... removed from logs ...", + }, + config: { + timeout: response.config.timeout, + headers: { + ...response.config.headers, + authorization: "... removed from logs ...", + }, + url: response.config.url, + method: response.config.method, + data: response.config.data, + }, + data: trimArraysToNElements(cloneDeep(response.data), 5), + }; }; /** @@ -223,12 +260,12 @@ const _filterResponseDataForDebug = (response) => { * @private */ const _throttle = (response) => { - return new Promise((resolve) => { - resolve(response); // Comment this line and uncomment bellow code if you want to test throttling in plugins api execution - // setTimeout(() => { - // resolve(response); - // }, _randomNumberBetween(2, 10) * 1000); - }); + return new Promise((resolve) => { + resolve(response); // Comment this line and uncomment bellow code if you want to test throttling in plugins api execution + // setTimeout(() => { + // resolve(response); + // }, _randomNumberBetween(2, 10) * 1000); + }); }; /** @@ -240,16 +277,16 @@ const _throttle = (response) => { * @private */ const _randomNumberBetween = (minBoundary, maxBoundary) => { - const _min = Math.ceil(minBoundary); - const _max = Math.ceil(maxBoundary); + const _min = Math.ceil(minBoundary); + const _max = Math.ceil(maxBoundary); - return Math.floor(Math.random() * (_max - _min + 1) + _min); + return Math.floor(Math.random() * (_max - _min + 1) + _min); }; module.exports = { - httpGetReq, - httpPostReq, - httpPatchReq, - httpPutReq, - httpDeleteReq + httpGetReq, + httpPostReq, + httpPatchReq, + httpPutReq, + httpDeleteReq, }; diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/job-helpers.plugins.js b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/job-helpers.plugins.js index d9cd33e559..b68694f5ba 100644 --- a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/job-helpers.plugins.js +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/job-helpers.plugins.js @@ -3,14 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); -const { Logger } = require('./logger-helpers.plugins'); +const { Logger } = require("./logger-helpers.plugins"); -const { applyGlobalEnvSettings } = require('./util-helpers.plugins'); +const { applyGlobalEnvSettings } = require("./util-helpers.plugins"); -const { httpDeleteReq, httpGetReq, httpPostReq, httpPatchReq } = require('./http-helpers.plugins'); +const { + httpDeleteReq, + httpGetReq, + httpPostReq, + httpPatchReq, +} = require("./http-helpers.plugins"); /** * ** Create and Deploy Data jobs if they don't exist. @@ -29,144 +34,187 @@ const { httpDeleteReq, httpGetReq, httpPostReq, httpPatchReq } = require('./http * @returns {Promise} */ const createDeployJobs = (taskConfig, config) => { - const startTime = new Date(); - - Logger.info('Trying to load Data jobs fixtures and binaries.'); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return ( - _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) - .then((commands) => { - return Promise.all( - // Send multiple parallel stream for Data jobs resolution - commands.map((command, index) => { - Logger.debug(`Data job fixture =>`, command.jobFixture); - - return ( - Promise - // Pass command down the stream - .resolve({ ...command, index }) - // Send requests to get Data job - .then((prevCommand) => { - return _getDataJobDeployments(prevCommand.jobFixture, config).then((response) => { - return { - ...prevCommand, - response - }; - }); - }) - // Check what is the state of requested Job, if it's deployed, or exist but not deployed, or doesn't exist at all - .then((prevCommand) => { - const jobFixture = prevCommand.jobFixture; - const jobName = jobFixture.job_name; - const httpResponse = prevCommand.response; - - let nextAction = 'done'; - - if (httpResponse.status === 200 && httpResponse.data.length > 0) { - Logger.info(`Data job "${jobName}" is already existing, therefore skipping creation and deployment.`); - } else if (httpResponse.status === 200 && httpResponse.data.length === 0) { - Logger.info(`Data job "${jobName}" exists, but it is not deployed. Will start deploying...`); - nextAction = 'deploy'; - } else if (httpResponse.status === 404) { - Logger.info(`Data job "${jobName}" not found. Will start creating...`); - nextAction = 'create'; - } - - return { - ...prevCommand, - nextAction - }; - }) - // Send request to create Job if its doesn't exist, otherwise pass execution to the next step - .then((prevCommand) => { - const currentAction = prevCommand.nextAction; - const jobFixture = prevCommand.jobFixture; - const jobName = jobFixture.job_name; - - if (currentAction === 'create') { - return _createDataJob(jobFixture, config).then((response) => { - if (response.status === 201) { - return { - ...prevCommand, - nextAction: 'deploy' - }; - } else { - Logger.error(`Failed to create Data job "${jobName}"`); - - throw new Error(`Failed to create Data job "${jobName}"`); - } - }); - } - - return { - ...prevCommand - }; - }) - // Send request to deploy Job if it was created on previous step - .then((prevCommandOuter) => { - const currentAction = prevCommandOuter.nextAction; - const jobFixture = prevCommandOuter.jobFixture; - const pathToZipFile = prevCommandOuter.pathToZipFile; - const jobName = jobFixture.job_name; - - if (currentAction === 'deploy') { - if (!pathToZipFile) { - Logger.info(`Data job "${jobName}" doesn't have path to job zip file, therefore skipping deployment.`); - } else { - Logger.debug(`Loading Job Zip binary located on =>`, pathToZipFile); - - let jobZipFile; - - try { - jobZipFile = fs.readFileSync(pathToZipFile, { encoding: 'base64' }); - } catch (error) { - Logger.error(`Cannot read file located on =>`, pathToZipFile); - - throw error; - } - - return new Promise((resolve) => { - setTimeout(() => resolve(prevCommandOuter), prevCommandOuter.index * 2000 + 250); - }).then((prevCommandInner) => { - return _deployDataJob(jobFixture, jobZipFile, config).then((result) => { - return { - ...prevCommandInner, - ...result - }; - }); - }); - } - } - - return { - ...prevCommandOuter, - code: 0 - }; - }) - // Final step to validate if everything is OK - .then((prevCommand) => { - if (prevCommand.code === 0) { - return true; - } - - const jobFixture = prevCommand.jobFixture; - const jobName = jobFixture.job_name; - - Logger.error(`Failed to create/deploy Data job "${jobName}"`); - - throw new Error(`Failed to create/deploy Data job "${jobName}".`); - }) + const startTime = new Date(); + + Logger.info("Trying to load Data jobs fixtures and binaries."); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return ( + _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) + .then((commands) => { + return Promise.all( + // Send multiple parallel stream for Data jobs resolution + commands.map((command, index) => { + Logger.debug(`Data job fixture =>`, command.jobFixture); + + return ( + Promise + // Pass command down the stream + .resolve({ ...command, index }) + // Send requests to get Data job + .then((prevCommand) => { + return _getDataJobDeployments( + prevCommand.jobFixture, + config, + ).then((response) => { + return { + ...prevCommand, + response, + }; + }); + }) + // Check what is the state of requested Job, if it's deployed, or exist but not deployed, or doesn't exist at all + .then((prevCommand) => { + const jobFixture = prevCommand.jobFixture; + const jobName = jobFixture.job_name; + const httpResponse = prevCommand.response; + + let nextAction = "done"; + + if ( + httpResponse.status === 200 && + httpResponse.data.length > 0 + ) { + Logger.info( + `Data job "${jobName}" is already existing, therefore skipping creation and deployment.`, + ); + } else if ( + httpResponse.status === 200 && + httpResponse.data.length === 0 + ) { + Logger.info( + `Data job "${jobName}" exists, but it is not deployed. Will start deploying...`, + ); + nextAction = "deploy"; + } else if (httpResponse.status === 404) { + Logger.info( + `Data job "${jobName}" not found. Will start creating...`, + ); + nextAction = "create"; + } + + return { + ...prevCommand, + nextAction, + }; + }) + // Send request to create Job if its doesn't exist, otherwise pass execution to the next step + .then((prevCommand) => { + const currentAction = prevCommand.nextAction; + const jobFixture = prevCommand.jobFixture; + const jobName = jobFixture.job_name; + + if (currentAction === "create") { + return _createDataJob(jobFixture, config).then( + (response) => { + if (response.status === 201) { + return { + ...prevCommand, + nextAction: "deploy", + }; + } else { + Logger.error( + `Failed to create Data job "${jobName}"`, + ); + + throw new Error( + `Failed to create Data job "${jobName}"`, + ); + } + }, + ); + } + + return { + ...prevCommand, + }; + }) + // Send request to deploy Job if it was created on previous step + .then((prevCommandOuter) => { + const currentAction = prevCommandOuter.nextAction; + const jobFixture = prevCommandOuter.jobFixture; + const pathToZipFile = prevCommandOuter.pathToZipFile; + const jobName = jobFixture.job_name; + + if (currentAction === "deploy") { + if (!pathToZipFile) { + Logger.info( + `Data job "${jobName}" doesn't have path to job zip file, therefore skipping deployment.`, + ); + } else { + Logger.debug( + `Loading Job Zip binary located on =>`, + pathToZipFile, + ); + + let jobZipFile; + + try { + jobZipFile = fs.readFileSync(pathToZipFile, { + encoding: "base64", + }); + } catch (error) { + Logger.error( + `Cannot read file located on =>`, + pathToZipFile, ); - }) - ); - }) - // Deployment to its end for all Jobs - .finally(() => { - const endTime = new Date(); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - }) - ); + + throw error; + } + + return new Promise((resolve) => { + setTimeout( + () => resolve(prevCommandOuter), + prevCommandOuter.index * 2000 + 250, + ); + }).then((prevCommandInner) => { + return _deployDataJob( + jobFixture, + jobZipFile, + config, + ).then((result) => { + return { + ...prevCommandInner, + ...result, + }; + }); + }); + } + } + + return { + ...prevCommandOuter, + code: 0, + }; + }) + // Final step to validate if everything is OK + .then((prevCommand) => { + if (prevCommand.code === 0) { + return true; + } + + const jobFixture = prevCommand.jobFixture; + const jobName = jobFixture.job_name; + + Logger.error(`Failed to create/deploy Data job "${jobName}"`); + + throw new Error( + `Failed to create/deploy Data job "${jobName}".`, + ); + }) + ); + }), + ); + }) + // Deployment to its end for all Jobs + .finally(() => { + const endTime = new Date(); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + }) + ); }; /** @@ -186,120 +234,158 @@ const createDeployJobs = (taskConfig, config) => { * @returns {Promise} */ const provideDataJobsExecutions = (taskConfig, config) => { - const startTime = new Date(); - const jobExecutionTimeout = 180000; // Wait up to 3 min per Job for execution to complete - - Logger.info(`Trying to provide executions for Data job fixtures`); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return ( - _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) - .then((commands) => { - // Send requests to get data for all the Jobs - return Promise.all( - commands.map((command, index) => { - Logger.debug(`Data job fixture =>`, command.jobFixture); - - const jobFixture = command.jobFixture; - const jobName = jobFixture.job_name; - const targetExecutions = taskConfig.relativePathToFixtures[index]?.executions ?? 2; - - Logger.info(`Trying to provide at least ${targetExecutions} executions for Data job ${jobName}`); - - // Pass the command down the stream with timeouts to avoid glitch on the server - return ( - new Promise((resolve) => { - setTimeout(() => resolve({ ...command }), index * 2150 + 350); - }) - // Get Data job executions - .then((prevCommandOuter) => { - return _getDataJobExecutionsArray(prevCommandOuter.jobFixture, config).then((executions) => { - Logger.info(`For Data job "${jobName}" found ${executions.length} executions, while target is ${targetExecutions}`); - - if (executions.length >= targetExecutions) { - Logger.info(`For Data job "${jobName}" skipping serial execution, because expected number found`); - - if (executions.length > targetExecutions) { - return { - ...prevCommandOuter, - executions, - code: 0 - }; - } - - // Wait to finish if there is already executing Data job and continue - return waitForDataJobExecutionToComplete(jobFixture, jobExecutionTimeout, config).then(() => { - return { - ...prevCommandOuter, - executions, - code: 0 - }; - }); - } - - // Wait to finish if there is already executing Data job and continue - return ( - waitForDataJobExecutionToComplete(jobFixture, jobExecutionTimeout, config) - .then(() => { - return { - ...prevCommandOuter, - executions, - code: 0 - }; - }) - // Get Data job last deployment - .then((prevCommandInner) => { - return _getJobLastDeployment(jobFixture, config).then((lastDeployment) => { - return { - ...prevCommandInner, - lastDeployment, - code: 0 - }; - }); - }) - // If last deployment found trigger execution for Data job - .then((prevCommandInner) => { - if (prevCommandInner.lastDeployment) { - return _triggerExecutionForJob(jobFixture, jobExecutionTimeout, prevCommandInner.lastDeployment, targetExecutions - prevCommandInner.executions.length, config).then((result) => { - return { - ...prevCommandInner, - ...result - }; - }); - } else { - Logger.error(`For Data job "${jobName}" last deployment was not found`); - } - - return { - ...prevCommandInner, - code: 1 - }; - }) - ); - }); - }) - // Final step to validate if everything is OK - .then((prevCommand) => { - if (prevCommand.code === 0) { - Logger.info(`Provided at least ${targetExecutions} executions for Data job fixtures`); - - return true; - } - - Logger.error(`Failed to provide at least ${targetExecutions} for Data job "${jobName}"`); - - throw new Error(`Failed to provide at least ${targetExecutions} for Data job "${jobName}"`); - }) - ); - }) - ); - }) - // Executions to its end for all Jobs - .finally(() => { - const endTime = new Date(); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - }) - ); + const startTime = new Date(); + const jobExecutionTimeout = 180000; // Wait up to 3 min per Job for execution to complete + + Logger.info(`Trying to provide executions for Data job fixtures`); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return ( + _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) + .then((commands) => { + // Send requests to get data for all the Jobs + return Promise.all( + commands.map((command, index) => { + Logger.debug(`Data job fixture =>`, command.jobFixture); + + const jobFixture = command.jobFixture; + const jobName = jobFixture.job_name; + const targetExecutions = + taskConfig.relativePathToFixtures[index]?.executions ?? 2; + + Logger.info( + `Trying to provide at least ${targetExecutions} executions for Data job ${jobName}`, + ); + + // Pass the command down the stream with timeouts to avoid glitch on the server + return ( + new Promise((resolve) => { + setTimeout(() => resolve({ ...command }), index * 2150 + 350); + }) + // Get Data job executions + .then((prevCommandOuter) => { + return _getDataJobExecutionsArray( + prevCommandOuter.jobFixture, + config, + ).then((executions) => { + Logger.info( + `For Data job "${jobName}" found ${executions.length} executions, while target is ${targetExecutions}`, + ); + + if (executions.length >= targetExecutions) { + Logger.info( + `For Data job "${jobName}" skipping serial execution, because expected number found`, + ); + + if (executions.length > targetExecutions) { + return { + ...prevCommandOuter, + executions, + code: 0, + }; + } + + // Wait to finish if there is already executing Data job and continue + return waitForDataJobExecutionToComplete( + jobFixture, + jobExecutionTimeout, + config, + ).then(() => { + return { + ...prevCommandOuter, + executions, + code: 0, + }; + }); + } + + // Wait to finish if there is already executing Data job and continue + return ( + waitForDataJobExecutionToComplete( + jobFixture, + jobExecutionTimeout, + config, + ) + .then(() => { + return { + ...prevCommandOuter, + executions, + code: 0, + }; + }) + // Get Data job last deployment + .then((prevCommandInner) => { + return _getJobLastDeployment(jobFixture, config).then( + (lastDeployment) => { + return { + ...prevCommandInner, + lastDeployment, + code: 0, + }; + }, + ); + }) + // If last deployment found trigger execution for Data job + .then((prevCommandInner) => { + if (prevCommandInner.lastDeployment) { + return _triggerExecutionForJob( + jobFixture, + jobExecutionTimeout, + prevCommandInner.lastDeployment, + targetExecutions - + prevCommandInner.executions.length, + config, + ).then((result) => { + return { + ...prevCommandInner, + ...result, + }; + }); + } else { + Logger.error( + `For Data job "${jobName}" last deployment was not found`, + ); + } + + return { + ...prevCommandInner, + code: 1, + }; + }) + ); + }); + }) + // Final step to validate if everything is OK + .then((prevCommand) => { + if (prevCommand.code === 0) { + Logger.info( + `Provided at least ${targetExecutions} executions for Data job fixtures`, + ); + + return true; + } + + Logger.error( + `Failed to provide at least ${targetExecutions} for Data job "${jobName}"`, + ); + + throw new Error( + `Failed to provide at least ${targetExecutions} for Data job "${jobName}"`, + ); + }) + ); + }), + ); + }) + // Executions to its end for all Jobs + .finally(() => { + const endTime = new Date(); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + }) + ); }; /** @@ -319,49 +405,69 @@ const provideDataJobsExecutions = (taskConfig, config) => { * @returns {Promise} */ const changeJobsStatusesFixtures = (taskConfig, config) => { - const startTime = new Date(); - - Logger.info(`Trying to disable Data Jobs for provided Data Jobs fixtures`); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return ( - _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) - .then((commands) => { - const jobFixtures = commands.map((command) => applyGlobalEnvSettings(command.jobFixture)); - - return Promise.all( - jobFixtures.map((jobFixture) => { - _getJobLastDeployment(jobFixture, config).then((lastDeployment) => { - if (!lastDeployment || !lastDeployment.job_version) { - Logger.error(`Deployment doesn't exist for Data Job "${jobFixture?.job_name}", skipping status change`); - - return false; - } - - const deploymentHash = lastDeployment.job_version; - - return _updateDataJob(jobFixture.team, jobFixture.job_name, deploymentHash, { enabled: taskConfig.status }, config).then((updateResponse) => { - if (updateResponse.status >= 200 && updateResponse.status < 300) { - Logger.info(`Data Job "${jobFixture?.job_name}" status changed to "${taskConfig.status}"`); - - return true; - } + const startTime = new Date(); + + Logger.info(`Trying to disable Data Jobs for provided Data Jobs fixtures`); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return ( + _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) + .then((commands) => { + const jobFixtures = commands.map((command) => + applyGlobalEnvSettings(command.jobFixture), + ); + + return Promise.all( + jobFixtures.map((jobFixture) => { + _getJobLastDeployment(jobFixture, config).then((lastDeployment) => { + if (!lastDeployment || !lastDeployment.job_version) { + Logger.error( + `Deployment doesn't exist for Data Job "${jobFixture?.job_name}", skipping status change`, + ); - Logger.error(`Data Job "${jobFixture?.job_name}" failed status change to "${taskConfig.status}"`); + return false; + } + + const deploymentHash = lastDeployment.job_version; + + return _updateDataJob( + jobFixture.team, + jobFixture.job_name, + deploymentHash, + { enabled: taskConfig.status }, + config, + ).then((updateResponse) => { + if ( + updateResponse.status >= 200 && + updateResponse.status < 300 + ) { + Logger.info( + `Data Job "${jobFixture?.job_name}" status changed to "${taskConfig.status}"`, + ); + + return true; + } - return false; - }); - }); - }) + Logger.error( + `Data Job "${jobFixture?.job_name}" failed status change to "${taskConfig.status}"`, ); - }) - .then((responses) => responses.map((value) => !!value)) - // Status change to its end for all Jobs - .finally(() => { - const endTime = new Date(); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - }) - ); + + return false; + }); + }); + }), + ); + }) + .then((responses) => responses.map((value) => !!value)) + // Status change to its end for all Jobs + .finally(() => { + const endTime = new Date(); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + }) + ); }; /** @@ -382,46 +488,51 @@ const changeJobsStatusesFixtures = (taskConfig, config) => { * @returns {Promise} */ const deleteJobsFixtures = (taskConfig, config) => { - const startTime = new Date(); - - Logger.info(`Trying to delete Data Jobs for provided Data Jobs fixtures`); - Logger.profiling(`Start time: ${startTime.toISOString()}`); - - return ( - _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) - .then((commands) => { - const jobFixtures = commands.map((command) => command.jobFixture); - - // Send requests to delete data for fixtures - return deleteJobs(jobFixtures, config).then((states) => { - const successfulDeletion = []; - const unsuccessfulDeletion = []; - - states.forEach((isSuccessful, index) => { - if (isSuccessful) { - successfulDeletion.push(jobFixtures[index].job_name); - } else { - unsuccessfulDeletion.push(jobFixtures[index].job_name); - } - }); + const startTime = new Date(); + + Logger.info(`Trying to delete Data Jobs for provided Data Jobs fixtures`); + Logger.profiling(`Start time: ${startTime.toISOString()}`); + + return ( + _loadFixturesValuesAndBinariesPaths(taskConfig.relativePathToFixtures) + .then((commands) => { + const jobFixtures = commands.map((command) => command.jobFixture); + + // Send requests to delete data for fixtures + return deleteJobs(jobFixtures, config).then((states) => { + const successfulDeletion = []; + const unsuccessfulDeletion = []; + + states.forEach((isSuccessful, index) => { + if (isSuccessful) { + successfulDeletion.push(jobFixtures[index].job_name); + } else { + unsuccessfulDeletion.push(jobFixtures[index].job_name); + } + }); - if (successfulDeletion.length > 0) { - Logger.info(`Deleted Data Jobs: ${successfulDeletion.toString()}`); - } + if (successfulDeletion.length > 0) { + Logger.info(`Deleted Data Jobs: ${successfulDeletion.toString()}`); + } - if (!taskConfig.optional && unsuccessfulDeletion.length > 0) { - Logger.error(`Failed to delete Data Jobs: ${unsuccessfulDeletion.toString()}`); - } + if (!taskConfig.optional && unsuccessfulDeletion.length > 0) { + Logger.error( + `Failed to delete Data Jobs: ${unsuccessfulDeletion.toString()}`, + ); + } - return true; - }); - }) - // Executions to its end for all Jobs - .finally(() => { - const endTime = new Date(); - Logger.profiling(`End time: ${endTime.toISOString()};`, `Duration: ${(endTime - startTime) / 1000}s`); - }) - ); + return true; + }); + }) + // Executions to its end for all Jobs + .finally(() => { + const endTime = new Date(); + Logger.profiling( + `End time: ${endTime.toISOString()};`, + `Duration: ${(endTime - startTime) / 1000}s`, + ); + }) + ); }; /** @@ -432,39 +543,39 @@ const deleteJobsFixtures = (taskConfig, config) => { * @returns {Promise} */ const deleteJobs = (jobFixtures, config) => { - return Promise.all( - jobFixtures.map((injectedJobFixture) => { - const jobFixture = applyGlobalEnvSettings(injectedJobFixture); - const jobName = jobFixture.job_name; + return Promise.all( + jobFixtures.map((injectedJobFixture) => { + const jobFixture = applyGlobalEnvSettings(injectedJobFixture); + const jobName = jobFixture.job_name; - Logger.info(`Trying to delete Data job "${jobName}"`); + Logger.info(`Trying to delete Data job "${jobName}"`); - return _getDataJob(jobFixture, config).then((outerResponse) => { - if (outerResponse.status === 200) { - return _deleteDataJob(jobFixture, config).then((innerResponse) => { - if (innerResponse.status === 200) { - Logger.info(`Data job "${jobName}" deleted`); + return _getDataJob(jobFixture, config).then((outerResponse) => { + if (outerResponse.status === 200) { + return _deleteDataJob(jobFixture, config).then((innerResponse) => { + if (innerResponse.status === 200) { + Logger.info(`Data job "${jobName}" deleted`); - return true; - } + return true; + } - Logger.error(`Data job "${jobName}" delete failed`); + Logger.error(`Data job "${jobName}" delete failed`); - return false; - }); - } + return false; + }); + } - if (outerResponse.status === 404) { - Logger.info(`Data job "${jobName}" doesn't exist`); - } else { - Logger.info(`Data job "${jobName}" failed to get job details`); - Logger.error(`Data job "${jobName}" delete failed`); - } + if (outerResponse.status === 404) { + Logger.info(`Data job "${jobName}" doesn't exist`); + } else { + Logger.info(`Data job "${jobName}" failed to get job details`); + Logger.error(`Data job "${jobName}" delete failed`); + } - return false; - }); - }) - ).then((responses) => responses.map((value) => !!value)); + return false; + }); + }), + ).then((responses) => responses.map((value) => !!value)); }; /** @@ -475,55 +586,74 @@ const deleteJobs = (jobFixtures, config) => { * @param {Cypress.ResolvedConfigOptions} config * @returns {Promise<{code: number}>} */ -const waitForDataJobExecutionToComplete = (injectedJobFixture, jobExecutionTimeout, config) => { - const jobFixture = applyGlobalEnvSettings(injectedJobFixture); - const jobName = jobFixture.job_name; - const pollInterval = 10000; // retry every 10s - - return _getDataJobExecutions(jobFixture, config).then((response) => { - if (response.status !== 200) { - Logger.error(`Failed Data job "${jobName}" executions polling`); - - return { - code: 0 - }; - } +const waitForDataJobExecutionToComplete = ( + injectedJobFixture, + jobExecutionTimeout, + config, +) => { + const jobFixture = applyGlobalEnvSettings(injectedJobFixture); + const jobName = jobFixture.job_name; + const pollInterval = 10000; // retry every 10s + + return _getDataJobExecutions(jobFixture, config).then((response) => { + if (response.status !== 200) { + Logger.error(`Failed Data job "${jobName}" executions polling`); + + return { + code: 0, + }; + } - const jobExecutions = response.data.sort(compareDatesASC); + const jobExecutions = response.data.sort(compareDatesASC); - if (jobExecutions.length === 0) { - Logger.info(`There is no Data job "${jobName}" execution to wait`); + if (jobExecutions.length === 0) { + Logger.info(`There is no Data job "${jobName}" execution to wait`); - return { - code: 0 - }; - } + return { + code: 0, + }; + } - const lastExecution = jobExecutions[jobExecutions.length - 1]; - const lastExecutionStatus = lastExecution.status.toLowerCase(); + const lastExecution = jobExecutions[jobExecutions.length - 1]; + const lastExecutionStatus = lastExecution.status.toLowerCase(); - if (lastExecutionStatus !== 'running' && lastExecutionStatus !== 'submitted') { - Logger.info(`Data job "${jobName}" executed successfully, polling completed`); + if ( + lastExecutionStatus !== "running" && + lastExecutionStatus !== "submitted" + ) { + Logger.info( + `Data job "${jobName}" executed successfully, polling completed`, + ); - return { - code: 0 - }; - } + return { + code: 0, + }; + } - if (jobExecutionTimeout <= 0) { - Logger.error(`Data job "${jobName}" waiting time for execution exceeded, skipping and continue with next steps`); + if (jobExecutionTimeout <= 0) { + Logger.error( + `Data job "${jobName}" waiting time for execution exceeded, skipping and continue with next steps`, + ); - return { - code: 1 - }; - } + return { + code: 1, + }; + } - Logger.info(`Data job "${jobName}", still executing... retry after ${pollInterval / 1000} seconds`); + Logger.info( + `Data job "${jobName}", still executing... retry after ${pollInterval / 1000} seconds`, + ); - return new Promise((resolve) => { - setTimeout(() => resolve(), pollInterval); - }).then(() => waitForDataJobExecutionToComplete(jobFixture, jobExecutionTimeout - pollInterval, config)); - }); + return new Promise((resolve) => { + setTimeout(() => resolve(), pollInterval); + }).then(() => + waitForDataJobExecutionToComplete( + jobFixture, + jobExecutionTimeout - pollInterval, + config, + ), + ); + }); }; /** @@ -533,26 +663,35 @@ const waitForDataJobExecutionToComplete = (injectedJobFixture, jobExecutionTimeo * @return {Promise} */ const deleteDataJobsWithoutDeployment = (config) => { - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/cy-e2e-vdk/jobs?operationName=jobsQuery&variables=%7B%22pageNumber%22:1,%22pageSize%22:100,%22filter%22:%5B%5D,%22search%22:%22%22%7D&query=query%20jobsQuery($filter:%20%5BPredicate%5D,%20$search:%20String,%20$pageNumber:%20Int,%20$pageSize:%20Int)%20%7B%0A%20%20jobs(%0A%20%20%20%20pageNumber:%20$pageNumber%0A%20%20%20%20pageSize:%20$pageSize%0A%20%20%20%20filter:%20$filter%0A%20%20%20%20search:%20$search%0A%20%20)%20%7B%0A%20%20%20%20content%20%7B%0A%20%20%20%20%20%20jobName%0A%20%20%20%20%20%20config%20%7B%0A%20%20%20%20%20%20%20%20team%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20deployments%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20enabled%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20totalPages%0A%20%20%20%20totalItems%0A%20%20%7D%0A%7D`; - - return httpGetReq(url).then((response) => { - const jobs = response.data.data.content; - const remappedJobs = jobs - .filter((job) => !job.deployments && job.jobName?.includes('cy-e2e-vdk')) - .map((job) => { - return { - job_name: job.jobName, - team: job.config?.team ?? 'cy-e2e-vdk' - }; - }); - - // log which Data Jobs should be deleted - remappedJobs.forEach((job, index) => { - Logger.debug('Deleting', index + 1, 'jobName:', job.jobName, 'team:', job?.config?.team, 'deployments:', job.deployments); - }); + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/cy-e2e-vdk/jobs?operationName=jobsQuery&variables=%7B%22pageNumber%22:1,%22pageSize%22:100,%22filter%22:%5B%5D,%22search%22:%22%22%7D&query=query%20jobsQuery($filter:%20%5BPredicate%5D,%20$search:%20String,%20$pageNumber:%20Int,%20$pageSize:%20Int)%20%7B%0A%20%20jobs(%0A%20%20%20%20pageNumber:%20$pageNumber%0A%20%20%20%20pageSize:%20$pageSize%0A%20%20%20%20filter:%20$filter%0A%20%20%20%20search:%20$search%0A%20%20)%20%7B%0A%20%20%20%20content%20%7B%0A%20%20%20%20%20%20jobName%0A%20%20%20%20%20%20config%20%7B%0A%20%20%20%20%20%20%20%20team%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20deployments%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20enabled%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20totalPages%0A%20%20%20%20totalItems%0A%20%20%7D%0A%7D`; - return _deleteDataJobsWithoutDeployment(remappedJobs, config); + return httpGetReq(url).then((response) => { + const jobs = response.data.data.content; + const remappedJobs = jobs + .filter((job) => !job.deployments && job.jobName?.includes("cy-e2e-vdk")) + .map((job) => { + return { + job_name: job.jobName, + team: job.config?.team ?? "cy-e2e-vdk", + }; + }); + + // log which Data Jobs should be deleted + remappedJobs.forEach((job, index) => { + Logger.debug( + "Deleting", + index + 1, + "jobName:", + job.jobName, + "team:", + job?.config?.team, + "deployments:", + job.deployments, + ); }); + + return _deleteDataJobsWithoutDeployment(remappedJobs, config); + }); }; /** @@ -564,10 +703,10 @@ const deleteDataJobsWithoutDeployment = (config) => { * @private */ const compareDatesASC = (left, right) => { - const leftStartTime = left.start_time ? left.start_time : 0; - const rightStartTime = right.start_time ? right.start_time : 0; + const leftStartTime = left.start_time ? left.start_time : 0; + const rightStartTime = right.start_time ? right.start_time : 0; - return new Date(leftStartTime).getTime() - new Date(rightStartTime).getTime(); + return new Date(leftStartTime).getTime() - new Date(rightStartTime).getTime(); }; /** @@ -582,32 +721,58 @@ const compareDatesASC = (left, right) => { * @returns {Promise<{code: number}>} * @private */ -const _triggerExecutionForJob = (jobFixture, jobExecutionTimeout, lastDeployment, neededExecutions, config, counterOfExecutions = 0) => { - const jobName = jobFixture.job_name; - - if (counterOfExecutions === 0) { - Logger.info(`Submitting ${neededExecutions} serial executions for Data job "${jobName}"`); - } else { - Logger.info(`Submitted ${counterOfExecutions} executions for Data job "${jobName}" and left ${neededExecutions - counterOfExecutions}`); - } +const _triggerExecutionForJob = ( + jobFixture, + jobExecutionTimeout, + lastDeployment, + neededExecutions, + config, + counterOfExecutions = 0, +) => { + const jobName = jobFixture.job_name; + + if (counterOfExecutions === 0) { + Logger.info( + `Submitting ${neededExecutions} serial executions for Data job "${jobName}"`, + ); + } else { + Logger.info( + `Submitted ${counterOfExecutions} executions for Data job "${jobName}" and left ${neededExecutions - counterOfExecutions}`, + ); + } - if (neededExecutions === counterOfExecutions) { - return Promise.resolve({ code: 0 }); - } + if (neededExecutions === counterOfExecutions) { + return Promise.resolve({ code: 0 }); + } - // Trigger job execution - return _executeJob(jobFixture, lastDeployment.id, config) - .then((result) => { - if (result.code === 0) { - return new Promise((resolve) => { - setTimeout(() => resolve({ code: 0 }), 5000); - }); - } - - return result; - }) - .then(() => waitForDataJobExecutionToComplete(jobFixture, jobExecutionTimeout, config)) - .then(() => _triggerExecutionForJob(jobFixture, jobExecutionTimeout, lastDeployment, neededExecutions, config, counterOfExecutions + 1)); + // Trigger job execution + return _executeJob(jobFixture, lastDeployment.id, config) + .then((result) => { + if (result.code === 0) { + return new Promise((resolve) => { + setTimeout(() => resolve({ code: 0 }), 5000); + }); + } + + return result; + }) + .then(() => + waitForDataJobExecutionToComplete( + jobFixture, + jobExecutionTimeout, + config, + ), + ) + .then(() => + _triggerExecutionForJob( + jobFixture, + jobExecutionTimeout, + lastDeployment, + neededExecutions, + config, + counterOfExecutions + 1, + ), + ); }; /** @@ -620,21 +785,21 @@ const _triggerExecutionForJob = (jobFixture, jobExecutionTimeout, lastDeployment * @private */ const _executeJob = (jobFixture, deploymentId, config) => { - const jobName = jobFixture.job_name; - const teamName = jobFixture.team; - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`; + const jobName = jobFixture.job_name; + const teamName = jobFixture.team; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`; - Logger.info(`Submitting Data job execution for "${jobName}"`); + Logger.info(`Submitting Data job execution for "${jobName}"`); - return httpPostReq(url, {}).then((response) => { - if (response.status >= 400) { - Logger.error(`Failed to execute Data job "${jobName}"`); - } + return httpPostReq(url, {}).then((response) => { + if (response.status >= 400) { + Logger.error(`Failed to execute Data job "${jobName}"`); + } - return { - code: 0 - }; - }); + return { + code: 0, + }; + }); }; /** @@ -646,17 +811,17 @@ const _executeJob = (jobFixture, deploymentId, config) => { * @private */ const _getDataJobExecutionsArray = (jobFixture, config) => { - const jobName = jobFixture.job_name; + const jobName = jobFixture.job_name; - return _getDataJobExecutions(jobFixture, config).then((response) => { - if (response.status !== 200) { - Logger.error(`Failed to get Data job "${jobName}" executions`); + return _getDataJobExecutions(jobFixture, config).then((response) => { + if (response.status !== 200) { + Logger.error(`Failed to get Data job "${jobName}" executions`); - return []; - } + return []; + } - return response.data && response.data.length ? response.data : []; - }); + return response.data && response.data.length ? response.data : []; + }); }; /** @@ -668,17 +833,19 @@ const _getDataJobExecutionsArray = (jobFixture, config) => { * @private */ const _getJobLastDeployment = (jobFixture, config) => { - const jobName = jobFixture.job_name; + const jobName = jobFixture.job_name; - return _getDataJobDeployments(jobFixture, config).then((response) => { - if (response.status !== 200) { - Logger.error(`Failed to get Data job "${jobName}" deployments`); + return _getDataJobDeployments(jobFixture, config).then((response) => { + if (response.status !== 200) { + Logger.error(`Failed to get Data job "${jobName}" deployments`); - return null; - } + return null; + } - return response.data && response.data.length ? response.data[response.data.length - 1] : null; - }); + return response.data && response.data.length + ? response.data[response.data.length - 1] + : null; + }); }; /** @@ -691,54 +858,72 @@ const _getJobLastDeployment = (jobFixture, config) => { * @private */ const _deployDataJob = (jobFixture, jobZipFile, config) => { - const jobName = jobFixture.job_name; - const teamName = jobFixture.team; - const waitForJobDeploymentTimeout = 420000; // Wait up to 7 min for deployment to complete. - - Logger.info(`Deploying Data job "${jobName}"`); - - // Upload Data Job resources - return _sendDataJobResources(teamName, jobName, jobZipFile, config).then((response1) => { - if (response1.status > 400) { - Logger.error(`Failed to send Data job "${jobName}" resources to server`); + const jobName = jobFixture.job_name; + const teamName = jobFixture.team; + const waitForJobDeploymentTimeout = 420000; // Wait up to 7 min for deployment to complete. - return { - code: 1 - }; - } - - /** - * @type {{version_sha: string}} - */ - let jsonResponse; + Logger.info(`Deploying Data job "${jobName}"`); - try { - jsonResponse = response1.data; - } catch (error) { - Logger.error(`Cannot parse response and read SHA for deployed Data jobs resources, response =>`, response1); + // Upload Data Job resources + return _sendDataJobResources(teamName, jobName, jobZipFile, config).then( + (response1) => { + if (response1.status > 400) { + Logger.error( + `Failed to send Data job "${jobName}" resources to server`, + ); - throw error; - } + return { + code: 1, + }; + } - // Deploy data job - return new Promise((resolve) => { - setTimeout(() => resolve(), 1000); - }) - .then(() => _deployDataJobSHAVersion(teamName, jobName, jsonResponse.version_sha, config)) - .then((response2) => { - if (response2.status === 202) { - Logger.info(`Data job "${jobName}" deployment in progress...`); - - return _waitForDataJobDeploymentToComplete(jobFixture, waitForJobDeploymentTimeout, config); - } + /** + * @type {{version_sha: string}} + */ + let jsonResponse; - Logger.error(`Data job "${jobName}" deployment failed`); + try { + jsonResponse = response1.data; + } catch (error) { + Logger.error( + `Cannot parse response and read SHA for deployed Data jobs resources, response =>`, + response1, + ); - return { - code: 0 - }; - }); - }); + throw error; + } + + // Deploy data job + return new Promise((resolve) => { + setTimeout(() => resolve(), 1000); + }) + .then(() => + _deployDataJobSHAVersion( + teamName, + jobName, + jsonResponse.version_sha, + config, + ), + ) + .then((response2) => { + if (response2.status === 202) { + Logger.info(`Data job "${jobName}" deployment in progress...`); + + return _waitForDataJobDeploymentToComplete( + jobFixture, + waitForJobDeploymentTimeout, + config, + ); + } + + Logger.error(`Data job "${jobName}" deployment failed`); + + return { + code: 0, + }; + }); + }, + ); }; /** @@ -751,32 +936,42 @@ const _deployDataJob = (jobFixture, jobZipFile, config) => { * @private */ const _waitForDataJobDeploymentToComplete = (jobFixture, timeout, config) => { - const jobName = jobFixture.job_name; - const waitInterval = 10000; // retry every 10s + const jobName = jobFixture.job_name; + const waitInterval = 10000; // retry every 10s - return _getDataJobDeployments(jobFixture, config).then((resp) => { - if (resp.status === 200 && resp.data.length > 0) { - Logger.info(`Data job "${jobName}" deployment finished`); + return _getDataJobDeployments(jobFixture, config).then((resp) => { + if (resp.status === 200 && resp.data.length > 0) { + Logger.info(`Data job "${jobName}" deployment finished`); - return { - code: 0 - }; - } + return { + code: 0, + }; + } - if (timeout <= 0) { - Logger.error(`Data job "${jobName}" waiting time to deploy exceeded, skipping and continue with next steps`); + if (timeout <= 0) { + Logger.error( + `Data job "${jobName}" waiting time to deploy exceeded, skipping and continue with next steps`, + ); - return { - code: 0 - }; - } + return { + code: 0, + }; + } - Logger.info(`Data job "${jobName}", still deploying... retry after ${waitInterval / 1000} seconds`); + Logger.info( + `Data job "${jobName}", still deploying... retry after ${waitInterval / 1000} seconds`, + ); - return new Promise((resolve) => { - setTimeout(() => resolve(), waitInterval); - }).then(() => _waitForDataJobDeploymentToComplete(jobFixture, timeout - waitInterval, config)); - }); + return new Promise((resolve) => { + setTimeout(() => resolve(), waitInterval); + }).then(() => + _waitForDataJobDeploymentToComplete( + jobFixture, + timeout - waitInterval, + config, + ), + ); + }); }; /** @@ -788,13 +983,13 @@ const _waitForDataJobDeploymentToComplete = (jobFixture, timeout, config) => { * @private */ const _createDataJob = (jobFixture, config) => { - const teamName = jobFixture.team; - const jobName = jobFixture.job_name; - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs`; + const teamName = jobFixture.team; + const jobName = jobFixture.job_name; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs`; - Logger.debug(`Creating Data job "${jobName}" for Team "${teamName}"`); + Logger.debug(`Creating Data job "${jobName}" for Team "${teamName}"`); - return httpPostReq(url, jobFixture); + return httpPostReq(url, jobFixture); }; /** @@ -808,12 +1003,18 @@ const _createDataJob = (jobFixture, config) => { * @returns {Promise>>} * @private */ -const _updateDataJob = (teamName, jobName, deploymentHash, jobFixture, config) => { - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentHash}`; - - Logger.debug(`Updating Data job "${jobName}" for Team "${teamName}"`); - - return httpPatchReq(url, jobFixture); +const _updateDataJob = ( + teamName, + jobName, + deploymentHash, + jobFixture, + config, +) => { + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentHash}`; + + Logger.debug(`Updating Data job "${jobName}" for Team "${teamName}"`); + + return httpPatchReq(url, jobFixture); }; /** @@ -825,13 +1026,13 @@ const _updateDataJob = (teamName, jobName, deploymentHash, jobFixture, config) = * @private */ const _getDataJob = (jobFixture, config) => { - const teamName = jobFixture.team; - const jobName = jobFixture.job_name; - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}`; + const teamName = jobFixture.team; + const jobName = jobFixture.job_name; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}`; - Logger.debug(`Get Data job "${jobName}"`); + Logger.debug(`Get Data job "${jobName}"`); - return httpGetReq(url); + return httpGetReq(url); }; /** @@ -843,13 +1044,13 @@ const _getDataJob = (jobFixture, config) => { * @private */ const _deleteDataJob = (jobFixture, config) => { - const teamName = jobFixture.team; - const jobName = jobFixture.job_name; - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}`; + const teamName = jobFixture.team; + const jobName = jobFixture.job_name; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}`; - Logger.debug(`Delete Data job "${jobName}"`); + Logger.debug(`Delete Data job "${jobName}"`); - return httpDeleteReq(url); + return httpDeleteReq(url); }; /** @@ -861,13 +1062,13 @@ const _deleteDataJob = (jobFixture, config) => { * @private */ const _getDataJobExecutions = (jobFixture, config) => { - const jobName = jobFixture.job_name; - const teamName = jobFixture.team; - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions`; + const jobName = jobFixture.job_name; + const teamName = jobFixture.team; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions`; - Logger.debug(`Get Data job "${jobName}" executions`); + Logger.debug(`Get Data job "${jobName}" executions`); - return httpGetReq(url); + return httpGetReq(url); }; /** @@ -879,13 +1080,13 @@ const _getDataJobExecutions = (jobFixture, config) => { * @private */ const _getDataJobDeployments = (jobFixture, config) => { - const jobName = jobFixture.job_name; - const teamName = jobFixture.team; - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments`; + const jobName = jobFixture.job_name; + const teamName = jobFixture.team; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments`; - Logger.debug(`Get Data job "${jobName}" deployments`); + Logger.debug(`Get Data job "${jobName}" deployments`); - return httpGetReq(url); + return httpGetReq(url); }; /** @@ -899,20 +1100,23 @@ const _getDataJobDeployments = (jobFixture, config) => { * @private */ const _sendDataJobResources = (teamName, jobName, jobZipFile, config) => { - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}/sources`; - let buffer; - - try { - buffer = Buffer.from(jobZipFile, 'base64'); - } catch (error) { - Logger.error(`Cannot convert from base64 to Buffer Data job "${jobName}" zip file`, error); + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}/sources`; + let buffer; + + try { + buffer = Buffer.from(jobZipFile, "base64"); + } catch (error) { + Logger.error( + `Cannot convert from base64 to Buffer Data job "${jobName}" zip file`, + error, + ); - throw error; - } + throw error; + } - Logger.debug(`Sending Data job resources to API, buffer =>`, buffer); + Logger.debug(`Sending Data job resources to API, buffer =>`, buffer); - return httpPostReq(url, buffer, { 'Content-Type': '' }); + return httpPostReq(url, buffer, { "Content-Type": "" }); }; /** @@ -926,15 +1130,15 @@ const _sendDataJobResources = (teamName, jobName, jobZipFile, config) => { * @private */ const _deployDataJobSHAVersion = (teamName, jobName, shaVersion, config) => { - const url = `${config.env['data_jobs_url']}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments`; + const url = `${config.env["data_jobs_url"]}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments`; - Logger.debug(`Deploying Data job SHA version =>`, shaVersion); + Logger.debug(`Deploying Data job SHA version =>`, shaVersion); - return httpPostReq(url, { - job_version: shaVersion, - mode: 'release', - enabled: true - }); + return httpPostReq(url, { + job_version: shaVersion, + mode: "release", + enabled: true, + }); }; /** @@ -945,45 +1149,53 @@ const _deployDataJobSHAVersion = (teamName, jobName, shaVersion, config) => { * @private */ const _loadFixturesValuesAndBinariesPaths = (pathToFixtures) => { - return Promise.resolve( - pathToFixtures.map((relativePathToFixture) => { - return { - pathToFixture: path.join(__dirname, `../../fixtures/${relativePathToFixture.pathToFixture.replace(/^\//, '')}`), - pathToZipFile: relativePathToFixture.pathToZipFile ? path.join(__dirname, `../../fixtures/${relativePathToFixture.pathToZipFile.replace(/^\//, '')}`) : null - }; - }) - ).then((pathToFixturesAndBinaries) => { - // Resolving files and parsing to JSON and passing down the stream - return pathToFixturesAndBinaries.map((data) => { - const pathToFixture = data.pathToFixture; - - Logger.debug(`Loading Data job fixture located on =>`, pathToFixture); - - let jobFile; - let jobFixture; - - try { - jobFile = fs.readFileSync(pathToFixture, { encoding: 'utf8' }); - } catch (error) { - Logger.error(`Cannot read file located on =>`, pathToFixture); - - throw error; - } + return Promise.resolve( + pathToFixtures.map((relativePathToFixture) => { + return { + pathToFixture: path.join( + __dirname, + `../../fixtures/${relativePathToFixture.pathToFixture.replace(/^\//, "")}`, + ), + pathToZipFile: relativePathToFixture.pathToZipFile + ? path.join( + __dirname, + `../../fixtures/${relativePathToFixture.pathToZipFile.replace(/^\//, "")}`, + ) + : null, + }; + }), + ).then((pathToFixturesAndBinaries) => { + // Resolving files and parsing to JSON and passing down the stream + return pathToFixturesAndBinaries.map((data) => { + const pathToFixture = data.pathToFixture; + + Logger.debug(`Loading Data job fixture located on =>`, pathToFixture); + + let jobFile; + let jobFixture; + + try { + jobFile = fs.readFileSync(pathToFixture, { encoding: "utf8" }); + } catch (error) { + Logger.error(`Cannot read file located on =>`, pathToFixture); - try { - jobFixture = applyGlobalEnvSettings(JSON.parse(jobFile)); + throw error; + } - return { - jobFixture, - pathToZipFile: data.pathToZipFile - }; - } catch (error) { - Logger.error(`Cannot parse read file located on =>`, pathToFixture); + try { + jobFixture = applyGlobalEnvSettings(JSON.parse(jobFile)); - throw error; - } - }); + return { + jobFixture, + pathToZipFile: data.pathToZipFile, + }; + } catch (error) { + Logger.error(`Cannot parse read file located on =>`, pathToFixture); + + throw error; + } }); + }); }; /** @@ -995,27 +1207,32 @@ const _loadFixturesValuesAndBinariesPaths = (pathToFixtures) => { * @return {Promise} * @private */ -const _deleteDataJobsWithoutDeployment = (jobFixtures, config, responses = []) => { - const chunk = jobFixtures.length > 10 ? jobFixtures.splice(0, 10) : jobFixtures; - - return deleteJobs(chunk, config).then((statuses) => { - responses.push(...statuses); - - if (jobFixtures.length > 0) { - return _deleteDataJobsWithoutDeployment(jobFixtures, config, responses); - } +const _deleteDataJobsWithoutDeployment = ( + jobFixtures, + config, + responses = [], +) => { + const chunk = + jobFixtures.length > 10 ? jobFixtures.splice(0, 10) : jobFixtures; + + return deleteJobs(chunk, config).then((statuses) => { + responses.push(...statuses); + + if (jobFixtures.length > 0) { + return _deleteDataJobsWithoutDeployment(jobFixtures, config, responses); + } - return responses; - }); + return responses; + }); }; module.exports = { - createDeployJobs, - deleteJobsFixtures, - deleteJobs, - provideDataJobsExecutions, - changeJobsStatusesFixtures, - waitForDataJobExecutionToComplete, - deleteDataJobsWithoutDeployment, - compareDatesASC + createDeployJobs, + deleteJobsFixtures, + deleteJobs, + provideDataJobsExecutions, + changeJobsStatusesFixtures, + waitForDataJobExecutionToComplete, + deleteDataJobsWithoutDeployment, + compareDatesASC, }; diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/logger-helpers.plugins.js b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/logger-helpers.plugins.js index 698d4acac8..23fa2dcc15 100644 --- a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/logger-helpers.plugins.js +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/logger-helpers.plugins.js @@ -9,7 +9,7 @@ * @param args */ const consoleLog = (...args) => { - console.log('LOG --------->', ...args); + console.log("LOG --------->", ...args); }; /** @@ -18,7 +18,7 @@ const consoleLog = (...args) => { * @param args */ const consoleInfo = (...args) => { - console.log('INFO -------->', ...args); + console.log("INFO -------->", ...args); }; /** @@ -27,7 +27,7 @@ const consoleInfo = (...args) => { * @param args */ const consoleDebug = (...args) => { - console.log('DEBUG ------->', ...args); + console.log("DEBUG ------->", ...args); }; /** @@ -36,7 +36,7 @@ const consoleDebug = (...args) => { * @param args */ const consoleError = (...args) => { - console.log('ERROR ------->', ...args); + console.log("ERROR ------->", ...args); }; /** @@ -45,15 +45,15 @@ const consoleError = (...args) => { * @param args */ const consoleProfiling = (...args) => { - console.log('PROFILING ------->', ...args); + console.log("PROFILING ------->", ...args); }; module.exports = { - Logger: { - log: consoleLog, - info: consoleInfo, - debug: consoleDebug, - error: consoleError, - profiling: consoleProfiling - } + Logger: { + log: consoleLog, + info: consoleInfo, + debug: consoleDebug, + error: consoleError, + profiling: consoleProfiling, + }, }; diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/util-helpers.plugins.js b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/util-helpers.plugins.js index 4c4ab08d48..8ca5b1dd45 100644 --- a/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/util-helpers.plugins.js +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/helpers/util-helpers.plugins.js @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -const { v4 } = require('uuid'); +const { v4 } = require("uuid"); -const { Logger } = require('./logger-helpers.plugins'); +const { Logger } = require("./logger-helpers.plugins"); const JWT_TOKEN_REGEX = new RegExp(`([^\.]+)\.([^\.]+)\.([^\.]*)`); -const DEFAULT_TEST_ENV_VAR = 'lib'; +const DEFAULT_TEST_ENV_VAR = "lib"; /** * ** Generate UUID. @@ -18,15 +18,15 @@ const DEFAULT_TEST_ENV_VAR = 'lib'; * @returns {Cypress.Chainable<{uuid: string}>|{uuid: string}} */ const generateUUID = (asynchronous = false) => { - const uuid = v4(); + const uuid = v4(); - Logger.info(`Generated uuid ${uuid}`); + Logger.info(`Generated uuid ${uuid}`); - if (asynchronous) { - return Promise.resolve({ uuid }); - } + if (asynchronous) { + return Promise.resolve({ uuid }); + } - return { uuid }; + return { uuid }; }; /** @@ -37,23 +37,23 @@ const generateUUID = (asynchronous = false) => { * @returns {{header:string; claims:{[key: string]: any;}; signature:string;}} The decoded token. */ const parseJWTToken = (token) => { - const parts = (token && token.match(JWT_TOKEN_REGEX)) || null; - if (!parts) { - throw new Error('Invalid JWT Format'); - } - - const rawHeader = parts[1]; - const rawBody = parts[2]; - const signature = parts[3]; - - const header = JSON.parse(_toUnicodeString(rawHeader)); - const claims = JSON.parse(_toUnicodeString(rawBody)); - - return { - header, - claims, - signature - }; + const parts = (token && token.match(JWT_TOKEN_REGEX)) || null; + if (!parts) { + throw new Error("Invalid JWT Format"); + } + + const rawHeader = parts[1]; + const rawBody = parts[2]; + const signature = parts[3]; + + const header = JSON.parse(_toUnicodeString(rawHeader)); + const claims = JSON.parse(_toUnicodeString(rawBody)); + + return { + header, + claims, + signature, + }; }; /** @@ -64,31 +64,41 @@ const parseJWTToken = (token) => { * @param {string} injectedTestUid * @returns {string|object|any} */ -const applyGlobalEnvSettings = (loadedElement, injectedTestEnvVar = null, injectedTestUid = null) => { - const TEST_ENV_VAR = injectedTestEnvVar ?? _loadTestEnvironmentVar(); - const TEST_UID = injectedTestUid ?? _loadTestUid(); - - if (typeof loadedElement === 'string') { - return loadedElement.replace('$env-placeholder$', `${TEST_ENV_VAR}-${TEST_UID}`); - } - - if (typeof loadedElement === 'object') { - Object.entries(loadedElement).forEach(([key, value]) => { - if (typeof value === 'string') { - if (value.includes('$env-placeholder$')) { - value = value.replace('$env-placeholder$', `${TEST_ENV_VAR}-${TEST_UID}`); - } +const applyGlobalEnvSettings = ( + loadedElement, + injectedTestEnvVar = null, + injectedTestUid = null, +) => { + const TEST_ENV_VAR = injectedTestEnvVar ?? _loadTestEnvironmentVar(); + const TEST_UID = injectedTestUid ?? _loadTestUid(); + + if (typeof loadedElement === "string") { + return loadedElement.replace( + "$env-placeholder$", + `${TEST_ENV_VAR}-${TEST_UID}`, + ); + } + + if (typeof loadedElement === "object") { + Object.entries(loadedElement).forEach(([key, value]) => { + if (typeof value === "string") { + if (value.includes("$env-placeholder$")) { + value = value.replace( + "$env-placeholder$", + `${TEST_ENV_VAR}-${TEST_UID}`, + ); + } - loadedElement[key] = value; - } + loadedElement[key] = value; + } - if (typeof value === 'object') { - loadedElement[key] = applyGlobalEnvSettings(value); - } - }); - } + if (typeof value === "object") { + loadedElement[key] = applyGlobalEnvSettings(value); + } + }); + } - return loadedElement; + return loadedElement; }; /** @@ -98,35 +108,42 @@ const applyGlobalEnvSettings = (loadedElement, injectedTestEnvVar = null, inject * @param {number} numberOfArrayElements */ const trimArraysToNElements = (object, numberOfArrayElements) => { - if (typeof object === 'undefined' || object === null || Number.isNaN(object)) { - return object; - } - - if (object instanceof Array) { - if (object.length > numberOfArrayElements) { - const chunk = object.slice(0, numberOfArrayElements); + if ( + typeof object === "undefined" || + object === null || + Number.isNaN(object) + ) { + return object; + } - return [...chunk, `... ${object.length - numberOfArrayElements} more elements in the Array ...`]; - } + if (object instanceof Array) { + if (object.length > numberOfArrayElements) { + const chunk = object.slice(0, numberOfArrayElements); - return object; + return [ + ...chunk, + `... ${object.length - numberOfArrayElements} more elements in the Array ...`, + ]; } - if (typeof object === 'object') { - Object.entries(object).forEach(([key, value]) => { - if (object instanceof Array) { - value = trimArraysToNElements(value, numberOfArrayElements); + return object; + } + + if (typeof object === "object") { + Object.entries(object).forEach(([key, value]) => { + if (object instanceof Array) { + value = trimArraysToNElements(value, numberOfArrayElements); - object[key] = value; - } + object[key] = value; + } - if (typeof value === 'object') { - object[key] = trimArraysToNElements(value, numberOfArrayElements); - } - }); - } + if (typeof value === "object") { + object[key] = trimArraysToNElements(value, numberOfArrayElements); + } + }); + } - return object; + return object; }; /** @@ -139,15 +156,17 @@ const trimArraysToNElements = (object, numberOfArrayElements) => { * @private */ const _toUnicodeString = (encoded) => { - // URL-save Base64 strings do not contain padding `=` characters, so add them. - const missingPadding = encoded.length % 4; - if (missingPadding !== 0) { - encoded += '='.repeat(4 - missingPadding); - } - - // Additional URL-safe character replacement - encoded = encoded.replace(/-/g, '+').replace(/_/g, '/'); - return decodeURIComponent(Array.prototype.map.call(atob(encoded), _escapeMultibyteCharacter).join('')); + // URL-save Base64 strings do not contain padding `=` characters, so add them. + const missingPadding = encoded.length % 4; + if (missingPadding !== 0) { + encoded += "=".repeat(4 - missingPadding); + } + + // Additional URL-safe character replacement + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + return decodeURIComponent( + Array.prototype.map.call(atob(encoded), _escapeMultibyteCharacter).join(""), + ); }; /** @@ -158,7 +177,7 @@ const _toUnicodeString = (encoded) => { * @private */ const _escapeMultibyteCharacter = (c) => { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); }; /** @@ -167,52 +186,68 @@ const _escapeMultibyteCharacter = (c) => { * @private */ const _loadTestEnvironmentVar = () => { - /** - * @type {string} - */ - let testEnv; - - if (typeof process !== 'undefined' && process.env?.CYPRESS_test_environment) { - testEnv = process.env.CYPRESS_test_environment; - } else if (typeof Cypress !== 'undefined' && Cypress.env && Cypress.env('test_environment')) { - testEnv = Cypress?.env('test_environment'); - } - - if (!testEnv) { - Logger.info(`test_environment is not set in system env variable or Cypress env variable.`); - Logger.debug(`Because test_environment is not explicitly set will use default: ${DEFAULT_TEST_ENV_VAR}`); - - testEnv = DEFAULT_TEST_ENV_VAR; - } - - return testEnv; + /** + * @type {string} + */ + let testEnv; + + if (typeof process !== "undefined" && process.env?.CYPRESS_test_environment) { + testEnv = process.env.CYPRESS_test_environment; + } else if ( + typeof Cypress !== "undefined" && + Cypress.env && + Cypress.env("test_environment") + ) { + testEnv = Cypress?.env("test_environment"); + } + + if (!testEnv) { + Logger.info( + `test_environment is not set in system env variable or Cypress env variable.`, + ); + Logger.debug( + `Because test_environment is not explicitly set will use default: ${DEFAULT_TEST_ENV_VAR}`, + ); + + testEnv = DEFAULT_TEST_ENV_VAR; + } + + return testEnv; }; const _loadTestUid = () => { - /** - * @type {string} - */ - let guid; - - if (typeof process !== 'undefined' && process.env?.CYPRESS_test_guid) { - guid = process.env.CYPRESS_test_guid; - } else if (typeof Cypress !== 'undefined' && Cypress.env && Cypress.env('test_guid')) { - guid = Cypress.env('test_guid'); - } - - if (!guid) { - guid = '1a4d2540515640d3'; - - Logger.info(`test_guid is not set in system env variable or Cypress env variable.`); - Logger.debug(`Because test_guid is not explicitly set will use default constant: ${guid}`); - } - - return guid; + /** + * @type {string} + */ + let guid; + + if (typeof process !== "undefined" && process.env?.CYPRESS_test_guid) { + guid = process.env.CYPRESS_test_guid; + } else if ( + typeof Cypress !== "undefined" && + Cypress.env && + Cypress.env("test_guid") + ) { + guid = Cypress.env("test_guid"); + } + + if (!guid) { + guid = "1a4d2540515640d3"; + + Logger.info( + `test_guid is not set in system env variable or Cypress env variable.`, + ); + Logger.debug( + `Because test_guid is not explicitly set will use default constant: ${guid}`, + ); + } + + return guid; }; module.exports = { - generateUUID, - applyGlobalEnvSettings, - parseJWTToken, - trimArraysToNElements + generateUUID, + applyGlobalEnvSettings, + parseJWTToken, + trimArraysToNElements, }; diff --git a/projects/frontend/data-pipelines/gui/e2e/plugins/index.js b/projects/frontend/data-pipelines/gui/e2e/plugins/index.js index 10827b4667..5d72508861 100644 --- a/projects/frontend/data-pipelines/gui/e2e/plugins/index.js +++ b/projects/frontend/data-pipelines/gui/e2e/plugins/index.js @@ -17,15 +17,30 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) -const path = require('path'); +const path = require("path"); -const { install, ensureBrowserFlags } = require('@neuralegion/cypress-har-generator'); +const { + install, + ensureBrowserFlags, +} = require("@neuralegion/cypress-har-generator"); -const { generateUUID } = require('./helpers/util-helpers.plugins'); +const { generateUUID } = require("./helpers/util-helpers.plugins"); -const { getAccessTokenAsynchronous, setAccessTokenAsynchronous, getAccessTokenSynchronous } = require('./helpers/authentication-helpers.plugins'); +const { + getAccessTokenAsynchronous, + setAccessTokenAsynchronous, + getAccessTokenSynchronous, +} = require("./helpers/authentication-helpers.plugins"); -const { createDeployJobs, provideDataJobsExecutions, waitForDataJobExecutionToComplete, deleteJobs, deleteJobsFixtures, changeJobsStatusesFixtures, deleteDataJobsWithoutDeployment } = require('./helpers/job-helpers.plugins'); +const { + createDeployJobs, + provideDataJobsExecutions, + waitForDataJobExecutionToComplete, + deleteJobs, + deleteJobsFixtures, + changeJobsStatusesFixtures, + deleteDataJobsWithoutDeployment, +} = require("./helpers/job-helpers.plugins"); /** * @type {Cypress.PluginConfig} @@ -33,148 +48,158 @@ const { createDeployJobs, provideDataJobsExecutions, waitForDataJobExecutionToCo // eslint-disable-next-line no-unused-vars // `cypressConfig` is the resolved Cypress config module.exports = (on, cypressConfig) => { - const TASKS = { - /** - * ** Delete Data Jobs without deployment with parallelism. - * - * @return {Promise} - */ - deleteDataJobsWithoutDeployment: () => { - return deleteDataJobsWithoutDeployment({ ...cypressConfig }); - }, - /** - * ** Create test Data Jobs if they don't exist. - * - * @param {{relativePathToFixtures: Array<{pathToFixture:string; pathToZipFile:string;}>}} taskConfig - configuration for task. - * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures - * e.g. { - * relativePathToFixtures: [ - * { - * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json', - * pathToZipFile: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.zip' - * } - * ] - * } - * @returns {Promise} - */ - createDeployJobs: (taskConfig) => { - return createDeployJobs(taskConfig, { ...cypressConfig }); - }, - /** - * ** Provide Data Jobs executions. - * - * @param {{relativePathToFixtures: Array<{pathToFixture:string; executions?: number;}>;}} taskConfig - configuration for task. - * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures - * e.g. { - * relativePathToFixtures: [ - * { - * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json', - * executions: 2 - * } - * ] - * } - * @returns {Promise} - */ - provideDataJobsExecutions: (taskConfig) => { - return provideDataJobsExecutions(taskConfig, { ...cypressConfig }); - }, - /** - * ** Wait for Data Jobs execution for complete. - * - * @param {{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}} jobFixture - * @param {number} jobExecutionTimeout - job execution timeout - * @returns {Promise<{code: number}>} - */ - waitForDataJobExecutionToComplete: ({ jobFixture, jobExecutionTimeout = 180000 }) => { - return waitForDataJobExecutionToComplete(jobFixture, jobExecutionTimeout, { ...cypressConfig }); - }, - /** - * ** Change Data Jobs statuses for provided fixtures. - * - * @param {{relativePathToFixtures: Array<{pathToFixture:string;}>; status: boolean;}} taskConfig - configuration for task. - * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures - * e.g. { - * relativePathToFixtures: [ - * { - * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json' - * } - * ], - * status: boolean - * } - * @returns {Promise} - */ - changeJobsStatusesFixtures: (taskConfig) => { - return changeJobsStatusesFixtures(taskConfig, { ...cypressConfig }); - }, - /** - * ** Delete Jobs for provided fixtures paths. - * - * @param {{relativePathToFixtures: Array<{pathToFixture:string;}>; optional?: boolean;}} taskConfig - configuration for task. - * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures - * optional if set to true will instruct the plugin to not log console error, if deletion status is different from 2xx, (e.g. 404 or 500) - * e.g. { - * relativePathToFixtures: [ - * { - * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json' - * } - * ], - * optional: true - * } - * @returns {Promise} - */ - deleteJobsFixtures: (taskConfig) => { - return deleteJobsFixtures(taskConfig, { ...cypressConfig }); - }, - /** - * ** Delete Data jobs if they exist. - * - * @param {Array<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} jobFixtures - * @param {Cypress.ResolvedConfigOptions} config - * @returns {Promise} - */ - deleteJobs: ({ jobFixtures }) => { - return deleteJobs(jobFixtures, { ...cypressConfig }); - }, - /** - * ** Get JWT access token asynchronous. - */ - getAccessToken: getAccessTokenAsynchronous, - /** - * ** Set JWT access token asynchronous. - */ - setAccessToken: setAccessTokenAsynchronous, - /** - * ** Generate UUID. - * - * @returns {Cypress.Chainable<{uuid: string}>} - */ - generateUUID: () => { - return generateUUID(true); - } - }; + const TASKS = { + /** + * ** Delete Data Jobs without deployment with parallelism. + * + * @return {Promise} + */ + deleteDataJobsWithoutDeployment: () => { + return deleteDataJobsWithoutDeployment({ ...cypressConfig }); + }, + /** + * ** Create test Data Jobs if they don't exist. + * + * @param {{relativePathToFixtures: Array<{pathToFixture:string; pathToZipFile:string;}>}} taskConfig - configuration for task. + * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures + * e.g. { + * relativePathToFixtures: [ + * { + * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json', + * pathToZipFile: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.zip' + * } + * ] + * } + * @returns {Promise} + */ + createDeployJobs: (taskConfig) => { + return createDeployJobs(taskConfig, { ...cypressConfig }); + }, + /** + * ** Provide Data Jobs executions. + * + * @param {{relativePathToFixtures: Array<{pathToFixture:string; executions?: number;}>;}} taskConfig - configuration for task. + * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures + * e.g. { + * relativePathToFixtures: [ + * { + * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json', + * executions: 2 + * } + * ] + * } + * @returns {Promise} + */ + provideDataJobsExecutions: (taskConfig) => { + return provideDataJobsExecutions(taskConfig, { ...cypressConfig }); + }, + /** + * ** Wait for Data Jobs execution for complete. + * + * @param {{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}} jobFixture + * @param {number} jobExecutionTimeout - job execution timeout + * @returns {Promise<{code: number}>} + */ + waitForDataJobExecutionToComplete: ({ + jobFixture, + jobExecutionTimeout = 180000, + }) => { + return waitForDataJobExecutionToComplete( + jobFixture, + jobExecutionTimeout, + { ...cypressConfig }, + ); + }, + /** + * ** Change Data Jobs statuses for provided fixtures. + * + * @param {{relativePathToFixtures: Array<{pathToFixture:string;}>; status: boolean;}} taskConfig - configuration for task. + * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures + * e.g. { + * relativePathToFixtures: [ + * { + * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json' + * } + * ], + * status: boolean + * } + * @returns {Promise} + */ + changeJobsStatusesFixtures: (taskConfig) => { + return changeJobsStatusesFixtures(taskConfig, { ...cypressConfig }); + }, + /** + * ** Delete Jobs for provided fixtures paths. + * + * @param {{relativePathToFixtures: Array<{pathToFixture:string;}>; optional?: boolean;}} taskConfig - configuration for task. + * relativePathToFixtures provides relative paths for fixtures files starting from directory fixtures + * optional if set to true will instruct the plugin to not log console error, if deletion status is different from 2xx, (e.g. 404 or 500) + * e.g. { + * relativePathToFixtures: [ + * { + * pathToFixture: '/base/data-jobs/cy-e2e-vdk/cy-e2e-vdk-failing-v0.json' + * } + * ], + * optional: true + * } + * @returns {Promise} + */ + deleteJobsFixtures: (taskConfig) => { + return deleteJobsFixtures(taskConfig, { ...cypressConfig }); + }, + /** + * ** Delete Data jobs if they exist. + * + * @param {Array<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} jobFixtures + * @param {Cypress.ResolvedConfigOptions} config + * @returns {Promise} + */ + deleteJobs: ({ jobFixtures }) => { + return deleteJobs(jobFixtures, { ...cypressConfig }); + }, + /** + * ** Get JWT access token asynchronous. + */ + getAccessToken: getAccessTokenAsynchronous, + /** + * ** Set JWT access token asynchronous. + */ + setAccessToken: setAccessTokenAsynchronous, + /** + * ** Generate UUID. + * + * @returns {Cypress.Chainable<{uuid: string}>} + */ + generateUUID: () => { + return generateUUID(true); + }, + }; - // `on` is used to hook into various events Cypress emits - on('task', TASKS); + // `on` is used to hook into various events Cypress emits + on("task", TASKS); - const options = { - outputRoot: cypressConfig.env.CYPRESS_TERMINAL_LOGS, - // Used to trim the base path of specs and reduce nesting in the - // generated output directory. - specRoot: path.relative(cypressConfig.fileServerFolder, cypressConfig.integrationFolder), - outputTarget: { - 'cypress-logs|json': 'json' - }, - printLogsToConsole: 'always', - printLogsToFile: 'always', - includeSuccessfulHookLogs: true, - logToFilesOnAfterRun: true - }; + const options = { + outputRoot: cypressConfig.env.CYPRESS_TERMINAL_LOGS, + // Used to trim the base path of specs and reduce nesting in the + // generated output directory. + specRoot: path.relative( + cypressConfig.fileServerFolder, + cypressConfig.integrationFolder, + ), + outputTarget: { + "cypress-logs|json": "json", + }, + printLogsToConsole: "always", + printLogsToFile: "always", + includeSuccessfulHookLogs: true, + logToFilesOnAfterRun: true, + }; - require('cypress-terminal-report/src/installLogsPrinter')(on, options); - install(on, cypressConfig); + require("cypress-terminal-report/src/installLogsPrinter")(on, options); + install(on, cypressConfig); - on('before:browser:launch', (browser = {}, launchOptions) => { - ensureBrowserFlags(browser, launchOptions); - return launchOptions; - }); + on("before:browser:launch", (browser = {}, launchOptions) => { + ensureBrowserFlags(browser, launchOptions); + return launchOptions; + }); }; diff --git a/projects/frontend/data-pipelines/gui/e2e/support/commands.js b/projects/frontend/data-pipelines/gui/e2e/support/commands.js index 6547a3ef71..07126ef6c8 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/commands.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/commands.js @@ -5,10 +5,15 @@ /// -import 'cypress-localstorage-commands'; +import "cypress-localstorage-commands"; -import { parseJWTToken } from '../plugins/helpers/util-helpers.plugins'; -import { BASIC_AUTH_CONFIG, CSP_ACCESS_TOKEN_KEY, CSP_EXPIRES_AT_KEY, CSP_ID_TOKEN_KEY } from './helpers/constants.support'; +import { parseJWTToken } from "../plugins/helpers/util-helpers.plugins"; +import { + BASIC_AUTH_CONFIG, + CSP_ACCESS_TOKEN_KEY, + CSP_EXPIRES_AT_KEY, + CSP_ID_TOKEN_KEY, +} from "./helpers/constants.support"; let jwtToken; let idToken; @@ -24,256 +29,283 @@ let expiresAt; // }); const _persistCspDataToStorage = (payload) => { - if (payload) { - idToken = payload.id_token; - accessToken = payload.access_token; - expiresAt = JSON.stringify(Date.now() + payload.expires_in * 1000); - } - - window.localStorage.setItem(CSP_ID_TOKEN_KEY, idToken); - window.localStorage.setItem(CSP_ACCESS_TOKEN_KEY, accessToken); - window.localStorage.setItem(CSP_EXPIRES_AT_KEY, expiresAt); - - // Set the CSP token in the session storage to enable the Integration testing against Management UI - sessionStorage.setItem(CSP_ID_TOKEN_KEY, idToken); - sessionStorage.setItem(CSP_ACCESS_TOKEN_KEY, accessToken); - sessionStorage.setItem(CSP_EXPIRES_AT_KEY, expiresAt); + if (payload) { + idToken = payload.id_token; + accessToken = payload.access_token; + expiresAt = JSON.stringify(Date.now() + payload.expires_in * 1000); + } + + window.localStorage.setItem(CSP_ID_TOKEN_KEY, idToken); + window.localStorage.setItem(CSP_ACCESS_TOKEN_KEY, accessToken); + window.localStorage.setItem(CSP_EXPIRES_AT_KEY, expiresAt); + + // Set the CSP token in the session storage to enable the Integration testing against Management UI + sessionStorage.setItem(CSP_ID_TOKEN_KEY, idToken); + sessionStorage.setItem(CSP_ACCESS_TOKEN_KEY, accessToken); + sessionStorage.setItem(CSP_EXPIRES_AT_KEY, expiresAt); }; -Cypress.Commands.add('login', () => { - cy.log('Requesting JWT Token with refresh token'); - - return cy +Cypress.Commands.add("login", () => { + cy.log("Requesting JWT Token with refresh token"); + + return cy + .request({ + followRedirect: false, + failOnStatusCode: false, + method: "POST", + // See project README.md how to set this login url + url: `${Cypress.env("csp_url")}/csp/gateway/am/api/auth/api-tokens/authorize`, + form: true, + headers: { + // CSP staging is using CloudFront CDN that does not permit bot requests unless the below user agent is set + "user-agent": "csp-automation-tests", + }, + body: { + // See project README.md how to set this token + api_token: Cypress.env("OAUTH2_API_TOKEN"), + }, + }) + .its("body") + .then(($body) => { + const decodedAccessToken = parseJWTToken($body.access_token); + + if (!decodedAccessToken.claims.ovl) { + return { + ...$body, + decoded_access_token: decodedAccessToken, + }; + } + + cy.log("Requesting claims expansion"); + + return cy .request({ - followRedirect: false, - failOnStatusCode: false, - method: 'POST', - // See project README.md how to set this login url - url: `${Cypress.env('csp_url')}/csp/gateway/am/api/auth/api-tokens/authorize`, - form: true, - headers: { - // CSP staging is using CloudFront CDN that does not permit bot requests unless the below user agent is set - 'user-agent': 'csp-automation-tests' - }, - body: { - // See project README.md how to set this token - api_token: Cypress.env('OAUTH2_API_TOKEN') - } - }) - .its('body') - .then(($body) => { - const decodedAccessToken = parseJWTToken($body.access_token); - - if (!decodedAccessToken.claims.ovl) { - return { - ...$body, - decoded_access_token: decodedAccessToken - }; - } - - cy.log('Requesting claims expansion'); - - return cy - .request({ - method: 'GET', - url: `${Cypress.env('csp_url')}/csp/gateway/am/api/auth/token/expand-overflow-claims`, - headers: { - Authorization: `Bearer ${$body.access_token}`, - // CSP staging is using CloudFront CDN that does not permit bot requests unless the below user agent is set - 'user-agent': 'csp-automation-tests' - } - }) - .its('body') - .then((claims) => { - return { - ...$body, - decoded_access_token: { - ...decodedAccessToken, - claims - } - }; - }); + method: "GET", + url: `${Cypress.env("csp_url")}/csp/gateway/am/api/auth/token/expand-overflow-claims`, + headers: { + Authorization: `Bearer ${$body.access_token}`, + // CSP staging is using CloudFront CDN that does not permit bot requests unless the below user agent is set + "user-agent": "csp-automation-tests", + }, }) - .then((body) => { - jwtToken = JSON.stringify(body); + .its("body") + .then((claims) => { + return { + ...$body, + decoded_access_token: { + ...decodedAccessToken, + claims, + }, + }; + }); + }) + .then((body) => { + jwtToken = JSON.stringify(body); - _persistCspDataToStorage(body); + _persistCspDataToStorage(body); - cy.task('setAccessToken', accessToken); + cy.task("setAccessToken", accessToken); - cy.log('Logged in successfully.'); + cy.log("Logged in successfully."); - return cy.wrap({ - context: 'commands::login()', - action: 'continue' - }); - }); + return cy.wrap({ + context: "commands::login()", + action: "continue", + }); + }); }); -Cypress.Commands.add('wireUserSession', () => { - if (jwtToken) { - _persistCspDataToStorage(); +Cypress.Commands.add("wireUserSession", () => { + if (jwtToken) { + _persistCspDataToStorage(); - return cy.wrap({ - context: 'commands::1::wireUserSession()', - action: 'continue' - }); - } + return cy.wrap({ + context: "commands::1::wireUserSession()", + action: "continue", + }); + } - return cy.login(); + return cy.login(); }); -Cypress.Commands.add('recordHarIfSupported', () => { - if (Cypress.browser.name === 'chrome') { - return cy.recordHar({ - excludePaths: [ - // exclude from dev build vendor, scripts and polyfills bundles - /(vendor|scripts|polyfills)\.js$/, - - // exclude from dev/prod clarity styles - /clr-ui\.min\.css$/, - - // exclude global and grafana styles - /(styles|grafana.light)(\..*)?\.css$/, - - // exclude woff2 fonts - /.*\.woff2/, - - // SuperCollider js - // exclude from prod build - // main bundle "main.xyz.js" - // scripts bundle "scripts.xyz.js" - // polyfills bundle "polyfills.xyz.js" - // runtime bundle "runtime.xyz.js" - // lazy loaded modules "199.xyz.js" - /(main|scripts|polyfills|runtime|\d+)(\..*)?\.js$/, - - // Grafana embedded panel js - // vendors app bundle "vendors-app.xyz.js" - // app bundle "app.xyz.js" - // moment-app bundle "moment-app.xyz.js" - // angular-app bundle "angular-app.xyz.js" - // influxdbPlugin bundle "influxdbPlugin.xyz.js" - // SoloPanelPage bundle "SoloPanelPage.xyz.js" - /(vendors-app|app|moment-app|angular-app|influxdbPlugin|SoloPanelPage)(\..*)?\.js$/ - ] - }); - } - - return cy.wrap({ - context: 'commands::recordHarIfSupported()', - action: 'continue' +Cypress.Commands.add("recordHarIfSupported", () => { + if (Cypress.browser.name === "chrome") { + return cy.recordHar({ + excludePaths: [ + // exclude from dev build vendor, scripts and polyfills bundles + /(vendor|scripts|polyfills)\.js$/, + + // exclude from dev/prod clarity styles + /clr-ui\.min\.css$/, + + // exclude global and grafana styles + /(styles|grafana.light)(\..*)?\.css$/, + + // exclude woff2 fonts + /.*\.woff2/, + + // SuperCollider js + // exclude from prod build + // main bundle "main.xyz.js" + // scripts bundle "scripts.xyz.js" + // polyfills bundle "polyfills.xyz.js" + // runtime bundle "runtime.xyz.js" + // lazy loaded modules "199.xyz.js" + /(main|scripts|polyfills|runtime|\d+)(\..*)?\.js$/, + + // Grafana embedded panel js + // vendors app bundle "vendors-app.xyz.js" + // app bundle "app.xyz.js" + // moment-app bundle "moment-app.xyz.js" + // angular-app bundle "angular-app.xyz.js" + // influxdbPlugin bundle "influxdbPlugin.xyz.js" + // SoloPanelPage bundle "SoloPanelPage.xyz.js" + /(vendors-app|app|moment-app|angular-app|influxdbPlugin|SoloPanelPage)(\..*)?\.js$/, + ], }); + } + + return cy.wrap({ + context: "commands::recordHarIfSupported()", + action: "continue", + }); }); -Cypress.Commands.add('saveHarIfSupported', () => { - if (Cypress.browser.name === 'chrome') { - return cy.saveHar(); - } +Cypress.Commands.add("saveHarIfSupported", () => { + if (Cypress.browser.name === "chrome") { + return cy.saveHar(); + } - return cy.wrap({ - context: 'commands::saveHarIfSupported()', - action: 'continue' - }); + return cy.wrap({ + context: "commands::saveHarIfSupported()", + action: "continue", + }); }); // CSP -Cypress.Commands.add('initCspLoggedUserProfileGetReqInterceptor', () => { - return cy.intercept('GET', '**/csp/gateway/am/api/auth/token-public-key?format=jwks').as('cspLoggedUserProfileGetReq'); +Cypress.Commands.add("initCspLoggedUserProfileGetReqInterceptor", () => { + return cy + .intercept("GET", "**/csp/gateway/am/api/auth/token-public-key?format=jwks") + .as("cspLoggedUserProfileGetReq"); }); -Cypress.Commands.add('waitForCspLoggedUserProfileGetReqInterceptor', () => { - return cy.wait('@cspLoggedUserProfileGetReq'); +Cypress.Commands.add("waitForCspLoggedUserProfileGetReqInterceptor", () => { + return cy.wait("@cspLoggedUserProfileGetReq"); }); // Data Pipelines -Cypress.Commands.add('initDataJobsApiGetReqInterceptor', () => { - return cy.intercept('GET', '**/data-jobs/for-team/**').as('dataJobsApiGetReq'); +Cypress.Commands.add("initDataJobsApiGetReqInterceptor", () => { + return cy + .intercept("GET", "**/data-jobs/for-team/**") + .as("dataJobsApiGetReq"); }); -Cypress.Commands.add('waitForDataJobsApiGetReqInterceptor', () => { - return cy.wait('@dataJobsApiGetReq'); +Cypress.Commands.add("waitForDataJobsApiGetReqInterceptor", () => { + return cy.wait("@dataJobsApiGetReq"); }); -Cypress.Commands.add('initDataJobsExecutionsGetReqInterceptor', () => { - return cy.intercept('GET', '**/data-jobs/for-team/**', (req) => { - if (req && req.query && req.query.query && req.query.operationName === 'jobsQuery' && req.query.query.includes('executions') && req.query.query.includes('DataJobExecutionFilter') && req.query.query.includes('DataJobExecutionOrder')) { - req.alias = 'dataJobsExecutionsGetReq'; - } +Cypress.Commands.add("initDataJobsExecutionsGetReqInterceptor", () => { + return cy.intercept("GET", "**/data-jobs/for-team/**", (req) => { + if ( + req && + req.query && + req.query.query && + req.query.operationName === "jobsQuery" && + req.query.query.includes("executions") && + req.query.query.includes("DataJobExecutionFilter") && + req.query.query.includes("DataJobExecutionOrder") + ) { + req.alias = "dataJobsExecutionsGetReq"; + } - return req; - }); + return req; + }); }); -Cypress.Commands.add('initDataJobsSingleExecutionGetReqInterceptor', () => { - return cy.intercept('GET', '**/data-jobs/for-team/**/executions/**').as('dataJobsSingleExecutionGetReq'); +Cypress.Commands.add("initDataJobsSingleExecutionGetReqInterceptor", () => { + return cy + .intercept("GET", "**/data-jobs/for-team/**/executions/**") + .as("dataJobsSingleExecutionGetReq"); }); -Cypress.Commands.add('initDataJobExecutionPostReqInterceptor', () => { - return cy.intercept('POST', '**/data-jobs/for-team/**/executions').as('dataJobExecutionPostReq'); +Cypress.Commands.add("initDataJobExecutionPostReqInterceptor", () => { + return cy + .intercept("POST", "**/data-jobs/for-team/**/executions") + .as("dataJobExecutionPostReq"); }); -Cypress.Commands.add('waitForDataJobExecutionPostReqInterceptor', () => { - return cy.wait('@dataJobExecutionPostReq'); +Cypress.Commands.add("waitForDataJobExecutionPostReqInterceptor", () => { + return cy.wait("@dataJobExecutionPostReq"); }); -Cypress.Commands.add('initDataJobExecutionDeleteReqInterceptor', () => { - return cy.intercept('DELETE', '**/data-jobs/for-team/**/executions/**').as('dataJobExecutionDeleteReq'); +Cypress.Commands.add("initDataJobExecutionDeleteReqInterceptor", () => { + return cy + .intercept("DELETE", "**/data-jobs/for-team/**/executions/**") + .as("dataJobExecutionDeleteReq"); }); -Cypress.Commands.add('waitForDataJobExecutionDeleteReqInterceptor', () => { - return cy.wait('@dataJobExecutionDeleteReq'); +Cypress.Commands.add("waitForDataJobExecutionDeleteReqInterceptor", () => { + return cy.wait("@dataJobExecutionDeleteReq"); }); -Cypress.Commands.add('initDataJobPutReqInterceptor', () => { - return cy.intercept('PUT', '**/data-jobs/for-team/*/jobs/*').as('dataJobPutReq'); +Cypress.Commands.add("initDataJobPutReqInterceptor", () => { + return cy + .intercept("PUT", "**/data-jobs/for-team/*/jobs/*") + .as("dataJobPutReq"); }); -Cypress.Commands.add('waitForDataJobPutReqInterceptor', () => { - return cy.wait('@dataJobPutReq'); +Cypress.Commands.add("waitForDataJobPutReqInterceptor", () => { + return cy.wait("@dataJobPutReq"); }); -Cypress.Commands.add('initDataJobDeleteReqInterceptor', () => { - return cy.intercept('DELETE', '**/data-jobs/for-team/**/jobs/**').as('dataJobDeleteReq'); +Cypress.Commands.add("initDataJobDeleteReqInterceptor", () => { + return cy + .intercept("DELETE", "**/data-jobs/for-team/**/jobs/**") + .as("dataJobDeleteReq"); }); -Cypress.Commands.add('waitForDataJobDeleteReqInterceptor', () => { - return cy.wait('@dataJobDeleteReq'); +Cypress.Commands.add("waitForDataJobDeleteReqInterceptor", () => { + return cy.wait("@dataJobDeleteReq"); }); -Cypress.Commands.add('initDataJobDeploymentPatchReqInterceptor', () => { - return cy.intercept('PATCH', '**/data-jobs/for-team/**/deployments/**').as('dataJobDeploymentPatchReq'); +Cypress.Commands.add("initDataJobDeploymentPatchReqInterceptor", () => { + return cy + .intercept("PATCH", "**/data-jobs/for-team/**/deployments/**") + .as("dataJobDeploymentPatchReq"); }); -Cypress.Commands.add('waitForDataJobDeploymentPatchReqInterceptor', () => { - return cy.wait('@dataJobDeploymentPatchReq'); +Cypress.Commands.add("waitForDataJobDeploymentPatchReqInterceptor", () => { + return cy.wait("@dataJobDeploymentPatchReq"); }); // Generics -Cypress.Commands.add('waitForInterceptorWithRetry', (aliasName, retries, config) => { +Cypress.Commands.add( + "waitForInterceptorWithRetry", + (aliasName, retries, config) => { return cy.wait(aliasName).then((interception) => { - if (config.predicate(interception)) { - if (typeof config.onfulfill === 'function') { - config.onfulfill(interception); - } - - return cy.wrap(interception); + if (config.predicate(interception)) { + if (typeof config.onfulfill === "function") { + config.onfulfill(interception); } - if (retries > 0) { - return cy.waitForInterceptorWithRetry(aliasName, retries - 1, config); - } + return cy.wrap(interception); + } + + if (retries > 0) { + return cy.waitForInterceptorWithRetry(aliasName, retries - 1, config); + } }); -}); + }, +); // App Config -Cypress.Commands.add('appConfigInterceptorDisableExploreRoute', () => { - cy.intercept('GET', '/assets/data/appConfig.json', { - auth: BASIC_AUTH_CONFIG, - ignoreRoutes: ['explore/data-jobs', 'explore/data-jobs/:team/:job'], - ignoreComponents: ['explorePage'] - }); +Cypress.Commands.add("appConfigInterceptorDisableExploreRoute", () => { + cy.intercept("GET", "/assets/data/appConfig.json", { + auth: BASIC_AUTH_CONFIG, + ignoreRoutes: ["explore/data-jobs", "explore/data-jobs/:team/:job"], + ignoreComponents: ["explorePage"], + }); }); diff --git a/projects/frontend/data-pipelines/gui/e2e/support/helpers/constants.support.js b/projects/frontend/data-pipelines/gui/e2e/support/helpers/constants.support.js index 35d800de4c..6a16db9a63 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/helpers/constants.support.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/helpers/constants.support.js @@ -9,31 +9,31 @@ * ** CSP Id token key. * @type {string} */ -const CSP_ID_TOKEN_KEY = 'id_token'; +const CSP_ID_TOKEN_KEY = "id_token"; /** * ** CSP Access token key. * @type {string} */ -const CSP_ACCESS_TOKEN_KEY = 'access_token'; +const CSP_ACCESS_TOKEN_KEY = "access_token"; /** * ** CSP Token expires at key. * @type {string} */ -const CSP_EXPIRES_AT_KEY = 'expires_at'; +const CSP_EXPIRES_AT_KEY = "expires_at"; /** * ** Long-lived Team that owns Data jobs for Data Pipelines feature. * @type {'cy-e2e-vdk'} */ -const TEAM_VDK = 'cy-e2e-vdk'; +const TEAM_VDK = "cy-e2e-vdk"; /** * ** Long-lived Data job name owned from {@link: TEAM_VDK} * @type {'cy-e2e-vdk-failing-v0'} */ -const TEAM_VDK_DATA_JOB_FAILING = 'cy-e2e-vdk-failing-v0'; +const TEAM_VDK_DATA_JOB_FAILING = "cy-e2e-vdk-failing-v0"; /** * ** Short-lived Test Data job name v0 owned from {@link: TEAM_VDK} created in tests and deleted after suite. @@ -42,7 +42,7 @@ const TEAM_VDK_DATA_JOB_FAILING = 'cy-e2e-vdk-failing-v0'; * * @type {'cy-e2e-vdk-test-v0'} */ -const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0 = 'cy-e2e-vdk-test-v0'; +const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0 = "cy-e2e-vdk-test-v0"; /** * ** Short-lived Test Data job name v1 owned from {@link: TEAM_VDK} created in tests and deleted after suite. @@ -51,7 +51,7 @@ const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0 = 'cy-e2e-vdk-test-v0'; * * @type {'cy-e2e-vdk-test-v1'} */ -const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1 = 'cy-e2e-vdk-test-v1'; +const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1 = "cy-e2e-vdk-test-v1"; /** * ** Short-lived Test Data job name v2 owned from {@link: TEAM_VDK} created in tests and deleted after suite. @@ -60,7 +60,7 @@ const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1 = 'cy-e2e-vdk-test-v1'; * * @type {'cy-e2e-vdk-test-v2'} */ -const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2 = 'cy-e2e-vdk-test-v2'; +const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2 = "cy-e2e-vdk-test-v2"; /** * ** Short-lived Test Data job name v10 owned from {@link: TEAM_VDK} created in tests and deleted after suite. @@ -69,7 +69,7 @@ const TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2 = 'cy-e2e-vdk-test-v2'; * * @type {'cy-e2e-vdk-test-v10'} */ -const TEAM_VDK_DATA_JOB_TEST_V10 = 'cy-e2e-vdk-test-v10'; +const TEAM_VDK_DATA_JOB_TEST_V10 = "cy-e2e-vdk-test-v10"; /** * ** Short-lived Test Data job name v11 owned from {@link: TEAM_VDK} created in tests and deleted after suite. @@ -78,7 +78,7 @@ const TEAM_VDK_DATA_JOB_TEST_V10 = 'cy-e2e-vdk-test-v10'; * * @type {'cy-e2e-vdk-test-v11'} */ -const TEAM_VDK_DATA_JOB_TEST_V11 = 'cy-e2e-vdk-test-v11'; +const TEAM_VDK_DATA_JOB_TEST_V11 = "cy-e2e-vdk-test-v11"; /** * ** Short-lived Test Data job name v12 owned from {@link: TEAM_VDK} created in tests and deleted after suite. @@ -87,53 +87,58 @@ const TEAM_VDK_DATA_JOB_TEST_V11 = 'cy-e2e-vdk-test-v11'; * * @type {'cy-e2e-vdk-test-v12'} */ -const TEAM_VDK_DATA_JOB_TEST_V12 = 'cy-e2e-vdk-test-v12'; +const TEAM_VDK_DATA_JOB_TEST_V12 = "cy-e2e-vdk-test-v12"; const BASIC_AUTH_CONFIG = { - consoleCloudUrl: 'https://console-stg.cloud.vmware.com/', - orgLinkRoot: '/csp/gateway/am/api/orgs/', - authConfig: { - issuer: 'https://console-stg.cloud.vmware.com/csp/gateway/am/api/', - redirectUri: '$window.location.origin/', - skipIssuerCheck: true, - requestAccessToken: true, - oidc: true, - strictDiscoveryDocumentValidation: false, - clientId: '8qQgcmhhsXuhGJs58ZW1hQ86h3eZXTpBV6t', - responseType: 'code', - scope: 'openid ALL_PERMISSIONS customer_number group_names', - showDebugInformation: true, - silentRefreshRedirectUri: '$window.location.origin/silent-refresh.html', - useSilentRefresh: true, - silentRefreshTimeout: 5000, - timeoutFactor: 0.25, - sessionChecksEnabled: true, - clearHashAfterLogin: false, - logoutUrl: 'https://console-stg.cloud.vmware.com/csp/gateway/discovery?logout', - nonceStateSeparator: 'semicolon' - }, - resourceServer: { - allowedUrls: ['https://console-stg.cloud.vmware.com/', 'https://gaz-preview.csp-vidm-prod.com/', '/data-jobs'], - sendAccessToken: true - }, - refreshTokenConfig: { - start: 500, - remainingTime: 360, - checkInterval: 60 - } + consoleCloudUrl: "https://console-stg.cloud.vmware.com/", + orgLinkRoot: "/csp/gateway/am/api/orgs/", + authConfig: { + issuer: "https://console-stg.cloud.vmware.com/csp/gateway/am/api/", + redirectUri: "$window.location.origin/", + skipIssuerCheck: true, + requestAccessToken: true, + oidc: true, + strictDiscoveryDocumentValidation: false, + clientId: "8qQgcmhhsXuhGJs58ZW1hQ86h3eZXTpBV6t", + responseType: "code", + scope: "openid ALL_PERMISSIONS customer_number group_names", + showDebugInformation: true, + silentRefreshRedirectUri: "$window.location.origin/silent-refresh.html", + useSilentRefresh: true, + silentRefreshTimeout: 5000, + timeoutFactor: 0.25, + sessionChecksEnabled: true, + clearHashAfterLogin: false, + logoutUrl: + "https://console-stg.cloud.vmware.com/csp/gateway/discovery?logout", + nonceStateSeparator: "semicolon", + }, + resourceServer: { + allowedUrls: [ + "https://console-stg.cloud.vmware.com/", + "https://gaz-preview.csp-vidm-prod.com/", + "/data-jobs", + ], + sendAccessToken: true, + }, + refreshTokenConfig: { + start: 500, + remainingTime: 360, + checkInterval: 60, + }, }; module.exports = { - BASIC_AUTH_CONFIG, - CSP_ID_TOKEN_KEY, - CSP_ACCESS_TOKEN_KEY, - CSP_EXPIRES_AT_KEY, - TEAM_VDK, - TEAM_VDK_DATA_JOB_FAILING, - TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0, - TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1, - TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2, - TEAM_VDK_DATA_JOB_TEST_V10, - TEAM_VDK_DATA_JOB_TEST_V11, - TEAM_VDK_DATA_JOB_TEST_V12 + BASIC_AUTH_CONFIG, + CSP_ID_TOKEN_KEY, + CSP_ACCESS_TOKEN_KEY, + CSP_EXPIRES_AT_KEY, + TEAM_VDK, + TEAM_VDK_DATA_JOB_FAILING, + TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0, + TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1, + TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2, + TEAM_VDK_DATA_JOB_TEST_V10, + TEAM_VDK_DATA_JOB_TEST_V11, + TEAM_VDK_DATA_JOB_TEST_V12, }; diff --git a/projects/frontend/data-pipelines/gui/e2e/support/index.js b/projects/frontend/data-pipelines/gui/e2e/support/index.js index 98f58cbf51..f49fa2dd22 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/index.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/index.js @@ -19,33 +19,41 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands'; +import "./commands"; -require('@neuralegion/cypress-har-generator/commands'); +require("@neuralegion/cypress-har-generator/commands"); -const CYPRESS_GREP = require('cypress-grep'); -const CYPRESS_TERMINAL = require('cypress-terminal-report/src/installLogsCollector'); +const CYPRESS_GREP = require("cypress-grep"); +const CYPRESS_TERMINAL = require("cypress-terminal-report/src/installLogsCollector"); CYPRESS_GREP(); -const CYPRESS_TERMINAL_ERROR_BLACKLIST = ['NG0100: ExpressionChangedAfterItHasBeenCheckedError', '"url": "https://console-stg.cloud.vmware.com/csp/gateway/slc/api/principal/org/service-families"', 'error loading jwks,']; +const CYPRESS_TERMINAL_ERROR_BLACKLIST = [ + "NG0100: ExpressionChangedAfterItHasBeenCheckedError", + '"url": "https://console-stg.cloud.vmware.com/csp/gateway/slc/api/principal/org/service-families"', + "error loading jwks,", +]; CYPRESS_TERMINAL({ - filterLog: ([logType, message, severity]) => { - if (logType !== 'cons:error') { - return true; - } - - return severity !== 'error' || !message || !CYPRESS_TERMINAL_ERROR_BLACKLIST.some((value) => message.includes(value)); + filterLog: ([logType, message, severity]) => { + if (logType !== "cons:error") { + return true; } + + return ( + severity !== "error" || + !message || + !CYPRESS_TERMINAL_ERROR_BLACKLIST.some((value) => message.includes(value)) + ); + }, }); Cypress.Server.defaults({ - delay: 500, - force404: false, - ignore: (xhr) => { - return true; - } + delay: 500, + force404: false, + ignore: (xhr) => { + return true; + }, }); // Alternatively you can use CommonJS syntax: diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/base-page.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/base-page.po.js index 4869b97cd3..a4f301a755 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/base-page.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/base-page.po.js @@ -5,660 +5,706 @@ /// -const path = require('path'); +const path = require("path"); /** * ** Base page superclass of all page objects classes. */ export class BasePagePO { - static WAIT_AFTER_API_MODIFY_CALL = 1000; // ms - static WAIT_SMART_DELAY = 250; // ms - static WAIT_CLICK_THINK_TIME = 500; // ms - static WAIT_VIEW_TO_RENDER_SHORT = 600; // ms - static WAIT_VIEW_TO_RENDER_BASE = 1000; // ms - static WAIT_MODAL_TO_RENDER_BASE = 500; //ms - static WAIT_ACTION_THINK_TIME = 750; // ms - static WAIT_INITIAL_PAGE_LOAD = 2 * 1000; // ms (2s) - static WAIT_BEFORE_SUITE_START = 1.5 * 1000; // ms (1.5s) - static WAIT_SHORT_TASK = 60 * 1000; // ms (1m) - static WAIT_MEDIUM_TASK = 3 * 60 * 1000; // ms (3m) - static WAIT_LONG_TASK = 5 * 60 * 1000; // ms (5m) - static WAIT_EXTRA_LONG_TASK = 10 * 60 * 1000; // ms (10m) - - // Generics - - /** - * ** Acquire JWT access token from CSP console API and return Chainable. - * - * @returns {Cypress.Chainable} - */ - static login() { - return this.executeCypressCommand('login'); - } - - /** - ** Wire user session when reattach auth cookies if exist otherwise request auth token and then set auth cookies. - * - * @returns {Cypress.Chainable} - */ - static wireUserSession() { - return this.executeCypressCommand('wireUserSession'); - } - - /** - * ** Returns instance of the page object. - */ - static getPage() { - return new BasePagePO(); - } - - /** - * ** Returns current browser Url. - * - * @param {number} timeout - * @returns {Cypress.Chainable} - */ - static getCurrentUrl(timeout) { - return cy.url({ - timeout: timeout ?? Cypress.config().defaultCommandTimeout - }); - } - - /** - * ** Init Http requests interceptors. - */ - static initInterceptors() { - // CSP - this.executeCypressCommand('initCspLoggedUserProfileGetReqInterceptor'); - // Data Pipelines - this.executeCypressCommand('initDataJobsApiGetReqInterceptor'); - } - - /** - * ** Navigate to some url. - * - * @param {string} url - * @returns {Cypress.Chainable} - */ - static navigateToUrl(url) { - return cy.visit(url); - } - - /** - * ** Navigate to page object page url. - */ - static navigateTo(..._args) { - this.navigateToUrl('/'); - - this.waitForApplicationBootstrap(); - - return this.getPage(); - } - - static navigateToNoBootstrap() { - this.navigateToUrl('/'); - this.waitForViewToRenderShort(); - return this.getPage(); - } - - /** - * ** Navigate to page with provided nav link id through side menu navigation and return instance of page object. - * - * @param {string|null} navLinkId - * @param {'openExplore'|'openManage'} sideNavCommand - * @param {{before: () => void; after: () => void;}} interceptors - */ - static navigateWithSideMenu(navLinkId = null, sideNavCommand = null, interceptors = null) { - cy.visit('/'); - - if (interceptors && typeof interceptors.before === 'function') { - interceptors.before(); - } else { - this.waitForApplicationBootstrap(); - } - - if (navLinkId) { - const basePagePO = this.getPage(); - - if (sideNavCommand === 'openExplore') { - basePagePO.openSideMenuNavExplore(); - } else if (sideNavCommand === 'openManage') { - basePagePO.openSideMenuNavManage(); - } - - basePagePO.navigateToPage(navLinkId); - } - - if (interceptors && typeof interceptors.after === 'function') { - interceptors.after(); + static WAIT_AFTER_API_MODIFY_CALL = 1000; // ms + static WAIT_SMART_DELAY = 250; // ms + static WAIT_CLICK_THINK_TIME = 500; // ms + static WAIT_VIEW_TO_RENDER_SHORT = 600; // ms + static WAIT_VIEW_TO_RENDER_BASE = 1000; // ms + static WAIT_MODAL_TO_RENDER_BASE = 500; //ms + static WAIT_ACTION_THINK_TIME = 750; // ms + static WAIT_INITIAL_PAGE_LOAD = 2 * 1000; // ms (2s) + static WAIT_BEFORE_SUITE_START = 1.5 * 1000; // ms (1.5s) + static WAIT_SHORT_TASK = 60 * 1000; // ms (1m) + static WAIT_MEDIUM_TASK = 3 * 60 * 1000; // ms (3m) + static WAIT_LONG_TASK = 5 * 60 * 1000; // ms (5m) + static WAIT_EXTRA_LONG_TASK = 10 * 60 * 1000; // ms (10m) + + // Generics + + /** + * ** Acquire JWT access token from CSP console API and return Chainable. + * + * @returns {Cypress.Chainable} + */ + static login() { + return this.executeCypressCommand("login"); + } + + /** + ** Wire user session when reattach auth cookies if exist otherwise request auth token and then set auth cookies. + * + * @returns {Cypress.Chainable} + */ + static wireUserSession() { + return this.executeCypressCommand("wireUserSession"); + } + + /** + * ** Returns instance of the page object. + */ + static getPage() { + return new BasePagePO(); + } + + /** + * ** Returns current browser Url. + * + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + static getCurrentUrl(timeout) { + return cy.url({ + timeout: timeout ?? Cypress.config().defaultCommandTimeout, + }); + } + + /** + * ** Init Http requests interceptors. + */ + static initInterceptors() { + // CSP + this.executeCypressCommand("initCspLoggedUserProfileGetReqInterceptor"); + // Data Pipelines + this.executeCypressCommand("initDataJobsApiGetReqInterceptor"); + } + + /** + * ** Navigate to some url. + * + * @param {string} url + * @returns {Cypress.Chainable} + */ + static navigateToUrl(url) { + return cy.visit(url); + } + + /** + * ** Navigate to page object page url. + */ + static navigateTo(..._args) { + this.navigateToUrl("/"); + + this.waitForApplicationBootstrap(); + + return this.getPage(); + } + + static navigateToNoBootstrap() { + this.navigateToUrl("/"); + this.waitForViewToRenderShort(); + return this.getPage(); + } + + /** + * ** Navigate to page with provided nav link id through side menu navigation and return instance of page object. + * + * @param {string|null} navLinkId + * @param {'openExplore'|'openManage'} sideNavCommand + * @param {{before: () => void; after: () => void;}} interceptors + */ + static navigateWithSideMenu( + navLinkId = null, + sideNavCommand = null, + interceptors = null, + ) { + cy.visit("/"); + + if (interceptors && typeof interceptors.before === "function") { + interceptors.before(); + } else { + this.waitForApplicationBootstrap(); + } + + if (navLinkId) { + const basePagePO = this.getPage(); + + if (sideNavCommand === "openExplore") { + basePagePO.openSideMenuNavExplore(); + } else if (sideNavCommand === "openManage") { + basePagePO.openSideMenuNavManage(); + } + + basePagePO.navigateToPage(navLinkId); + } + + if (interceptors && typeof interceptors.after === "function") { + interceptors.after(); + } + + return this.getPage(); + } + + /** + * ** Execute Cypress command. + * + * @param {string} cypressCommand + * @param {number | null} numberOfExecution + * @param {number | null} additionalWait + * @returns {Cypress.Chainable<{context: string, action: string}>|Cypress.Chainable} + */ + static executeCypressCommand( + cypressCommand, + numberOfExecution = null, + additionalWait = null, + ) { + if (typeof numberOfExecution === "number") { + /** + * @type Cypress.Chainable + */ + let lastCommand; + + for (let i = 0; i < numberOfExecution; i++) { + lastCommand = cy[cypressCommand](); + } + + if (typeof additionalWait === "number") { + return cy.wait(additionalWait); + } + + if (lastCommand) { + return lastCommand; + } + + return cy.wrap({ + context: "BasePagePO::base-page.po::executeCypressCommand()", + action: "continue", + }); + } else { + return cy[cypressCommand]().then(($value) => { + if (typeof additionalWait === "number") { + return cy.wait(additionalWait); } - return this.getPage(); - } - - /** - * ** Execute Cypress command. - * - * @param {string} cypressCommand - * @param {number | null} numberOfExecution - * @param {number | null} additionalWait - * @returns {Cypress.Chainable<{context: string, action: string}>|Cypress.Chainable} - */ - static executeCypressCommand(cypressCommand, numberOfExecution = null, additionalWait = null) { - if (typeof numberOfExecution === 'number') { - /** - * @type Cypress.Chainable - */ - let lastCommand; - - for (let i = 0; i < numberOfExecution; i++) { - lastCommand = cy[cypressCommand](); - } - - if (typeof additionalWait === 'number') { - return cy.wait(additionalWait); + return $value; + }); + } + } + + /** + * ** Wait for CSP logged user Get req interceptor completion. + * + * @param {number} additionalWait + * @return {Cypress.Chainable} + */ + static waitForCspLoggedUserProfileGetReqInterceptor(additionalWait = 1000) { + return this.executeCypressCommand( + "waitForCspLoggedUserProfileGetReqInterceptor", + 1, + additionalWait, + ); + } + + /** + * ** Wait for Data Jobs req interceptor completion. + * + * @param {number} numberOfReqToWait + * @param {number} additionalWait + * @return {Cypress.Chainable} + */ + static waitForDataJobsApiGetReqInterceptor( + numberOfReqToWait = 1, + additionalWait = 1000, + ) { + return this.executeCypressCommand( + "waitForDataJobsApiGetReqInterceptor", + numberOfReqToWait, + additionalWait, + ); + } + + /** + * ** Wait for Application bootstrap. + * + * @return {Cypress.Chainable} + */ + static waitForApplicationBootstrap() { + return this.waitForCspLoggedUserProfileGetReqInterceptor(null) + .then(() => this.waitForViewToRenderShort()) + .then(() => this.getMainContainer().should("exist")); + } + + /** + * ** Wait for initial page load. + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + static waitForInitialPageLoad(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_INITIAL_PAGE_LOAD); + } + + /** + * ** Wait for View to render. + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + static waitForViewToRender(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_BASE); + } + + /** + * ** Wait for View to render short. + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + static waitForViewToRenderShort(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_SHORT); + } + + /** + * ** Start HAR recording. + * + * @returns {Cypress.Chainable} + */ + static recordHarIfSupported() { + return this.executeCypressCommand("recordHarIfSupported"); + } + + /** + * ** Save recorded HAR. + * + * @returns {Cypress.Chainable} + */ + static saveHarIfSupported() { + return this.executeCypressCommand("saveHarIfSupported"); + } + + // Selectors + + /** + * ** Get main container. + * + * @returns {Cypress.Chainable>} + */ + static getMainContainer() { + return cy.get("[data-automation=vdk]", { + timeout: BasePagePO.WAIT_SHORT_TASK, + }); + } + + /** + * ** Wait for Data Jobs req interceptor completion. + * + * @param {number} numberOfReqToWait + * @return {Cypress.Chainable} + */ + waitForDataJobsApiGetReqInterceptor(numberOfReqToWait = 1) { + return BasePagePO.waitForDataJobsApiGetReqInterceptor(numberOfReqToWait); + } + + /** + * ** Wait smart delay. + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForSmartDelay(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_SMART_DELAY); // ms + } + + /** + * ** Wait Initial page load to finish. + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForInitialPageLoad(factor = 1) { + return BasePagePO.waitForInitialPageLoad(factor); // ms + } + + /** + * ** Wait for view to render + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForViewToRender(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_BASE); // ms + } + + /** + * ** Wait for view to render + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForViewToRenderShort(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_SHORT); // ms + } + + /** + * ** Wait for modal to render + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForModalToRender(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_MODAL_TO_RENDER_BASE); // ms + } + + /** + * ** Wait before click (something like thinking time in real env). + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForClickThinkingTime(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_CLICK_THINK_TIME); // ms + } + + /** + * ** Wait for generic action similar to click thinking time. + * + * @param {number} factor + * @return {Cypress.Chainable} + */ + waitForActionThinkingTime(factor = 1) { + return cy.wait(factor * BasePagePO.WAIT_ACTION_THINK_TIME); // ms + } + + /** + * ** Wait until Data grid is loaded. + * + * @param {string} contextSelector + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + waitForGridToLoad(contextSelector, timeout = BasePagePO.WAIT_SHORT_TASK) { + return cy + .get(`${contextSelector} clr-datagrid[data-automation=clr-grid-loaded]`, { + timeout: this.resolveTimeout(timeout), + }) + .should("exist"); + } + + /** + * ** Returns current browser Url. + * + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + getCurrentUrl(timeout) { + return BasePagePO.getCurrentUrl(timeout); + } + + /** + * ** Returns current browser Url normalized. + * + * @param {{includeUrl?: boolean; includeBaseUrl?: boolean; includePathSegment?: boolean; includeQueryString?: boolean; decodeQueryString?: boolean;}} extras + * @returns {Cypress.Chainable<{pathSegment: string; queryParams: {[key: string]: string}; baseUrl?: string; url?: string}>} + */ + getCurrentUrlNormalized(extras = { includeQueryString: true }) { + return this.getCurrentUrl().then((locationHref) => { + const fullUrlSplit = locationHref.split("?"); + const url = fullUrlSplit[0]; + const queryString = fullUrlSplit[1] ?? ""; + const pathSegment = url.replace(Cypress.config().baseUrl, ""); + + /** + * @type {{pathSegment: string; queryParams: {[key: string]: string}; baseUrl?: string; url?: string}} + */ + const normalizedData = {}; + + if (extras.includeBaseUrl) { + normalizedData.baseUrl = Cypress.config().baseUrl; + } + + if (extras.includeUrl) { + normalizedData.url = url; + } + + if (extras.includePathSegment) { + normalizedData.pathSegment = `/${pathSegment.replace(/^\/+/, "")}`; + } + + if (extras.includeQueryString) { + normalizedData.queryParams = queryString + .split("&") + .map((queryStringChunk) => { + if (queryStringChunk.length === 0) { + return {}; } - if (lastCommand) { - return lastCommand; - } - - return cy.wrap({ - context: 'BasePagePO::base-page.po::executeCypressCommand()', - action: 'continue' - }); - } else { - return cy[cypressCommand]().then(($value) => { - if (typeof additionalWait === 'number') { - return cy.wait(additionalWait); - } - - return $value; - }); - } - } - - /** - * ** Wait for CSP logged user Get req interceptor completion. - * - * @param {number} additionalWait - * @return {Cypress.Chainable} - */ - static waitForCspLoggedUserProfileGetReqInterceptor(additionalWait = 1000) { - return this.executeCypressCommand('waitForCspLoggedUserProfileGetReqInterceptor', 1, additionalWait); - } - - /** - * ** Wait for Data Jobs req interceptor completion. - * - * @param {number} numberOfReqToWait - * @param {number} additionalWait - * @return {Cypress.Chainable} - */ - static waitForDataJobsApiGetReqInterceptor(numberOfReqToWait = 1, additionalWait = 1000) { - return this.executeCypressCommand('waitForDataJobsApiGetReqInterceptor', numberOfReqToWait, additionalWait); - } - - /** - * ** Wait for Application bootstrap. - * - * @return {Cypress.Chainable} - */ - static waitForApplicationBootstrap() { - return this.waitForCspLoggedUserProfileGetReqInterceptor(null) - .then(() => this.waitForViewToRenderShort()) - .then(() => this.getMainContainer().should('exist')); - } - - /** - * ** Wait for initial page load. - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - static waitForInitialPageLoad(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_INITIAL_PAGE_LOAD); - } - - /** - * ** Wait for View to render. - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - static waitForViewToRender(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_BASE); - } - - /** - * ** Wait for View to render short. - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - static waitForViewToRenderShort(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_SHORT); - } - - /** - * ** Start HAR recording. - * - * @returns {Cypress.Chainable} - */ - static recordHarIfSupported() { - return this.executeCypressCommand('recordHarIfSupported'); - } - - /** - * ** Save recorded HAR. - * - * @returns {Cypress.Chainable} - */ - static saveHarIfSupported() { - return this.executeCypressCommand('saveHarIfSupported'); - } - - // Selectors - - /** - * ** Get main container. - * - * @returns {Cypress.Chainable>} - */ - static getMainContainer() { - return cy.get('[data-automation=vdk]', { - timeout: BasePagePO.WAIT_SHORT_TASK - }); - } - - /** - * ** Wait for Data Jobs req interceptor completion. - * - * @param {number} numberOfReqToWait - * @return {Cypress.Chainable} - */ - waitForDataJobsApiGetReqInterceptor(numberOfReqToWait = 1) { - return BasePagePO.waitForDataJobsApiGetReqInterceptor(numberOfReqToWait); - } - - /** - * ** Wait smart delay. - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForSmartDelay(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_SMART_DELAY); // ms - } - - /** - * ** Wait Initial page load to finish. - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForInitialPageLoad(factor = 1) { - return BasePagePO.waitForInitialPageLoad(factor); // ms - } - - /** - * ** Wait for view to render - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForViewToRender(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_BASE); // ms - } - - /** - * ** Wait for view to render - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForViewToRenderShort(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_VIEW_TO_RENDER_SHORT); // ms - } - - /** - * ** Wait for modal to render - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForModalToRender(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_MODAL_TO_RENDER_BASE); // ms - } - - /** - * ** Wait before click (something like thinking time in real env). - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForClickThinkingTime(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_CLICK_THINK_TIME); // ms - } - - /** - * ** Wait for generic action similar to click thinking time. - * - * @param {number} factor - * @return {Cypress.Chainable} - */ - waitForActionThinkingTime(factor = 1) { - return cy.wait(factor * BasePagePO.WAIT_ACTION_THINK_TIME); // ms - } - - /** - * ** Wait until Data grid is loaded. - * - * @param {string} contextSelector - * @param {number} timeout - * @returns {Cypress.Chainable} - */ - waitForGridToLoad(contextSelector, timeout = BasePagePO.WAIT_SHORT_TASK) { - return cy.get(`${contextSelector} clr-datagrid[data-automation=clr-grid-loaded]`, { timeout: this.resolveTimeout(timeout) }).should('exist'); - } - - /** - * ** Returns current browser Url. - * - * @param {number} timeout - * @returns {Cypress.Chainable} - */ - getCurrentUrl(timeout) { - return BasePagePO.getCurrentUrl(timeout); - } - - /** - * ** Returns current browser Url normalized. - * - * @param {{includeUrl?: boolean; includeBaseUrl?: boolean; includePathSegment?: boolean; includeQueryString?: boolean; decodeQueryString?: boolean;}} extras - * @returns {Cypress.Chainable<{pathSegment: string; queryParams: {[key: string]: string}; baseUrl?: string; url?: string}>} - */ - getCurrentUrlNormalized(extras = { includeQueryString: true }) { - return this.getCurrentUrl().then((locationHref) => { - const fullUrlSplit = locationHref.split('?'); - const url = fullUrlSplit[0]; - const queryString = fullUrlSplit[1] ?? ''; - const pathSegment = url.replace(Cypress.config().baseUrl, ''); - - /** - * @type {{pathSegment: string; queryParams: {[key: string]: string}; baseUrl?: string; url?: string}} - */ - const normalizedData = {}; - - if (extras.includeBaseUrl) { - normalizedData.baseUrl = Cypress.config().baseUrl; - } - - if (extras.includeUrl) { - normalizedData.url = url; - } - - if (extras.includePathSegment) { - normalizedData.pathSegment = `/${pathSegment.replace(/^\/+/, '')}`; - } - - if (extras.includeQueryString) { - normalizedData.queryParams = queryString - .split('&') - .map((queryStringChunk) => { - if (queryStringChunk.length === 0) { - return {}; - } - - const splitParam = queryStringChunk.split('='); - - return { - [splitParam[0]]: extras.decodeQueryString ? decodeURIComponent(splitParam[1]) : splitParam[1] - }; - }) - .reduce((accumulator, currentValue) => { - return { - ...accumulator, - ...currentValue - }; - }, {}); - } - - return cy.wrap(normalizedData); - }); - } - - // Selectors - - /** - * ** Get Page Title. - * - * @param {number | undefined | null} timeout - * @returns {Cypress.Chainable>} - */ - getPageTitle(timeout) { - return cy.get('[data-cy=data-pipelines-page-title]', { - timeout: this.resolveTimeout(timeout ?? 30000) - }); - } - - /** - * ** Get side menu nav group btn for Explore - * - * @returns {Cypress.Chainable>} - */ - getSideMenuExploreGroupBtn() { - return cy.get('[data-cy=data-pipelines-nav-group-explore] button').should('exist').contains('Explore'); - } - - /** - * ** Get side menu nav group btn for Manage - * - * @returns {Cypress.Chainable>} - */ - getSideMenuManageGroupBtn() { - return cy.get('[data-cy=data-pipelines-nav-group-manage] button').should('exist').contains('Manage'); - } - - /** - * ** Get side menu navigation link with given id. - * - * @param {string} navLinkId - * @returns {Cypress.Chainable>} - */ - getSideMenuNavLink(navLinkId) { - return cy.get(`#${navLinkId} > span`); - } - - /** - * ** Get toast element. - * - * @param {number | undefined | null} timeout - * @returns {Cypress.Chainable>} - */ - getToast(timeout) { - return cy.get('vdk-toast-container vdk-toast', { - timeout: this.resolveTimeout(timeout) - }); - } - - /** - * ** Get Toast title. - * - * @param {number | undefined | null} timeout - * @returns {Cypress.Chainable>} - */ - getToastTitle(timeout) { - return this.getToast(timeout).get('.toast-title'); - } - - /** - * ** Get Toast dismiss button. - * - * @param {number | undefined | null} timeout - * @returns {Cypress.Chainable>} - */ - getToastDismiss(timeout) { - return this.getToast(timeout).get('.dismiss-bg'); - } - - /** - * ** Get Modal dialog. - * - * @param {number | undefined | null} timeout - * @returns {Cypress.Chainable>} - */ - getModal(timeout) { - return cy.get('clr-modal .modal-dialog', { - timeout: this.resolveTimeout(timeout) - }); - } - - /** - * ** Get DataGrid. - * - * - Override to select desired DataGrid. - * - * @param {string|null} contextSelector - * @returns {Cypress.Chainable>} - */ - getGrid(contextSelector = null) { - if (contextSelector) { - return cy.get(`${contextSelector} clr-datagrid`); - } - - return cy.get('clr-datagrid'); - } - - /** - * ** Get Cell from DataGrid using the searchQuery. - * - * @param {string} searchQuery - * @returns {Cypress.Chainable>} - */ - getGridCell(searchQuery) { - return this.getGrid().contains('clr-dg-cell', searchQuery); - } - - /** - * ** Get Row from DataGrid using the searchQuery that identifies one of the Cells. - * - * @param {string} searchQuery - * @returns {Cypress.Chainable>} - */ - getGridRow(searchQuery) { - return this.getGridCell(searchQuery).parents('clr-dg-row'); - } - - /** - * ** Get all Rows from DatGrid. - * - * @returns {Cypress.Chainable>} - */ - getGridRows() { - return this.getGrid().find('clr-dg-row'); - } - - /** - * ** Get select button from Row that is identified by the searchQuery. - * - * @param {string} searchQuery - * @returns {Cypress.Chainable>} - */ - getGridRowSelectBtn(searchQuery) { - return this.getGridRow(searchQuery).find('.datagrid-select input'); - } - - // Actions - - /** - * ** Open Explore side menu nav group. - * - * @return {Cypress.Chainable} - */ - openSideMenuNavExplore() { - return this.getSideMenuExploreGroupBtn() - .should('exist') - .scrollIntoView() - .click({ force: true }) - .then(() => this.waitForViewToRenderShort()); - } - - /** - * ** Open Manage side menu nav group. - * - * @return {Cypress.Chainable} - */ - openSideMenuNavManage() { - return this.getSideMenuManageGroupBtn() - .should('exist') - .scrollIntoView() - .click({ force: true }) - .then(() => this.waitForViewToRenderShort()); - } - - /** - * ** Navigate to Manage page through link with provided id. - * - * @param {string} navLinkId - * @returns {Cypress.Chainable} - */ - navigateToPage(navLinkId) { - return this.getSideMenuNavLink(navLinkId).should('exist').click({ force: true }); - } - - /** - * ** Resolve timeout using provided value or fallback to default value. - * - * @param {number | undefined | null} timeout - * @returns {number} - */ - resolveTimeout(timeout) { - return timeout === undefined ? Cypress.config('defaultCommandTimeout') : timeout; - } - - /** - * ** Read file with provided name in provided directory. - * - * @param {string} folderName - * @param {string} fileName - * @returns {Cypress.Chainable} - */ - readFile(folderName, fileName) { - const downloadsFolder = Cypress.config(folderName); - - return cy.readFile(path.join(downloadsFolder, fileName)); - } - - /** - * ** Clear local storage key if provided, otherwise clear everything. - * - * @param {string | undefined | null} key - */ - clearLocalStorage(key) { - if (key) { - cy.clearLocalStorage(key); - - return; - } - - cy.clearLocalStorage(); - } - - selectGridRow(searchQuery) { - this.getGridRowSelectBtn(searchQuery).should('exist').scrollIntoView().check({ force: true }); - - this.waitForActionThinkingTime(); - } - - /** - * ** Wait until Data grid is loaded. - * - * @param {'explore'|'manage'} contextSelector - * @param {number} timeout - * @returns {Cypress.Chainable} - */ - _waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { - return cy.get(`[data-cy=${contextSelector}] clr-datagrid[data-automation=clr-grid-loaded]`, { timeout: this.resolveTimeout(timeout) }).should('exist'); - } + const splitParam = queryStringChunk.split("="); + + return { + [splitParam[0]]: extras.decodeQueryString + ? decodeURIComponent(splitParam[1]) + : splitParam[1], + }; + }) + .reduce((accumulator, currentValue) => { + return { + ...accumulator, + ...currentValue, + }; + }, {}); + } + + return cy.wrap(normalizedData); + }); + } + + // Selectors + + /** + * ** Get Page Title. + * + * @param {number | undefined | null} timeout + * @returns {Cypress.Chainable>} + */ + getPageTitle(timeout) { + return cy.get("[data-cy=data-pipelines-page-title]", { + timeout: this.resolveTimeout(timeout ?? 30000), + }); + } + + /** + * ** Get side menu nav group btn for Explore + * + * @returns {Cypress.Chainable>} + */ + getSideMenuExploreGroupBtn() { + return cy + .get("[data-cy=data-pipelines-nav-group-explore] button") + .should("exist") + .contains("Explore"); + } + + /** + * ** Get side menu nav group btn for Manage + * + * @returns {Cypress.Chainable>} + */ + getSideMenuManageGroupBtn() { + return cy + .get("[data-cy=data-pipelines-nav-group-manage] button") + .should("exist") + .contains("Manage"); + } + + /** + * ** Get side menu navigation link with given id. + * + * @param {string} navLinkId + * @returns {Cypress.Chainable>} + */ + getSideMenuNavLink(navLinkId) { + return cy.get(`#${navLinkId} > span`); + } + + /** + * ** Get toast element. + * + * @param {number | undefined | null} timeout + * @returns {Cypress.Chainable>} + */ + getToast(timeout) { + return cy.get("vdk-toast-container vdk-toast", { + timeout: this.resolveTimeout(timeout), + }); + } + + /** + * ** Get Toast title. + * + * @param {number | undefined | null} timeout + * @returns {Cypress.Chainable>} + */ + getToastTitle(timeout) { + return this.getToast(timeout).get(".toast-title"); + } + + /** + * ** Get Toast dismiss button. + * + * @param {number | undefined | null} timeout + * @returns {Cypress.Chainable>} + */ + getToastDismiss(timeout) { + return this.getToast(timeout).get(".dismiss-bg"); + } + + /** + * ** Get Modal dialog. + * + * @param {number | undefined | null} timeout + * @returns {Cypress.Chainable>} + */ + getModal(timeout) { + return cy.get("clr-modal .modal-dialog", { + timeout: this.resolveTimeout(timeout), + }); + } + + /** + * ** Get DataGrid. + * + * - Override to select desired DataGrid. + * + * @param {string|null} contextSelector + * @returns {Cypress.Chainable>} + */ + getGrid(contextSelector = null) { + if (contextSelector) { + return cy.get(`${contextSelector} clr-datagrid`); + } + + return cy.get("clr-datagrid"); + } + + /** + * ** Get Cell from DataGrid using the searchQuery. + * + * @param {string} searchQuery + * @returns {Cypress.Chainable>} + */ + getGridCell(searchQuery) { + return this.getGrid().contains("clr-dg-cell", searchQuery); + } + + /** + * ** Get Row from DataGrid using the searchQuery that identifies one of the Cells. + * + * @param {string} searchQuery + * @returns {Cypress.Chainable>} + */ + getGridRow(searchQuery) { + return this.getGridCell(searchQuery).parents("clr-dg-row"); + } + + /** + * ** Get all Rows from DatGrid. + * + * @returns {Cypress.Chainable>} + */ + getGridRows() { + return this.getGrid().find("clr-dg-row"); + } + + /** + * ** Get select button from Row that is identified by the searchQuery. + * + * @param {string} searchQuery + * @returns {Cypress.Chainable>} + */ + getGridRowSelectBtn(searchQuery) { + return this.getGridRow(searchQuery).find(".datagrid-select input"); + } + + // Actions + + /** + * ** Open Explore side menu nav group. + * + * @return {Cypress.Chainable} + */ + openSideMenuNavExplore() { + return this.getSideMenuExploreGroupBtn() + .should("exist") + .scrollIntoView() + .click({ force: true }) + .then(() => this.waitForViewToRenderShort()); + } + + /** + * ** Open Manage side menu nav group. + * + * @return {Cypress.Chainable} + */ + openSideMenuNavManage() { + return this.getSideMenuManageGroupBtn() + .should("exist") + .scrollIntoView() + .click({ force: true }) + .then(() => this.waitForViewToRenderShort()); + } + + /** + * ** Navigate to Manage page through link with provided id. + * + * @param {string} navLinkId + * @returns {Cypress.Chainable} + */ + navigateToPage(navLinkId) { + return this.getSideMenuNavLink(navLinkId) + .should("exist") + .click({ force: true }); + } + + /** + * ** Resolve timeout using provided value or fallback to default value. + * + * @param {number | undefined | null} timeout + * @returns {number} + */ + resolveTimeout(timeout) { + return timeout === undefined + ? Cypress.config("defaultCommandTimeout") + : timeout; + } + + /** + * ** Read file with provided name in provided directory. + * + * @param {string} folderName + * @param {string} fileName + * @returns {Cypress.Chainable} + */ + readFile(folderName, fileName) { + const downloadsFolder = Cypress.config(folderName); + + return cy.readFile(path.join(downloadsFolder, fileName)); + } + + /** + * ** Clear local storage key if provided, otherwise clear everything. + * + * @param {string | undefined | null} key + */ + clearLocalStorage(key) { + if (key) { + cy.clearLocalStorage(key); + + return; + } + + cy.clearLocalStorage(); + } + + selectGridRow(searchQuery) { + this.getGridRowSelectBtn(searchQuery) + .should("exist") + .scrollIntoView() + .check({ force: true }); + + this.waitForActionThinkingTime(); + } + + /** + * ** Wait until Data grid is loaded. + * + * @param {'explore'|'manage'} contextSelector + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + _waitForGridToLoad( + contextSelector, + timeout = DataJobsBasePO.WAIT_SHORT_TASK, + ) { + return cy + .get( + `[data-cy=${contextSelector}] clr-datagrid[data-automation=clr-grid-loaded]`, + { timeout: this.resolveTimeout(timeout) }, + ) + .should("exist"); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-base.po.js index 7121cef42c..eb37ab4ac1 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-base.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-base.po.js @@ -5,401 +5,474 @@ /// -import { DataPipelinesBasePO } from './data-pipelines-base.po'; +import { DataPipelinesBasePO } from "./data-pipelines-base.po"; export class DataJobBasePO extends DataPipelinesBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobBasePO} - */ - static getPage() { - return new DataJobBasePO(); - } - - /** - * ** Init Http requests interceptors. - */ - static initInterceptors() { - super.initInterceptors(); - - this.executeCypressCommand('initDataJobsSingleExecutionGetReqInterceptor'); - this.executeCypressCommand('initDataJobsExecutionsGetReqInterceptor'); - this.executeCypressCommand('initDataJobPutReqInterceptor'); - this.executeCypressCommand('initDataJobDeleteReqInterceptor'); - this.executeCypressCommand('initDataJobExecutionDeleteReqInterceptor'); - } - - /** - * ** Navigate to Data Job Url. - * - * @param {'explore'|'manage'} context - * @param {string} teamName - * @param {string} jobName - * @param {'details'|'executions'|'lineage'} subpage - */ - static navigateTo(context, teamName, jobName, subpage) { - const numberOfDataJobsApiGetReqInterceptorWaiting = subpage === 'details' ? 4 : 3; - - return this.navigateToDataJobUrl(`/${context}/data-jobs/${teamName}/${jobName}/${subpage}`, numberOfDataJobsApiGetReqInterceptorWaiting); - } - - /** - * ** Wait for Data Job put req. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobPutReqInterceptor() { - return this.executeCypressCommand('waitForDataJobPutReqInterceptor', 1, 1000); - } - - /** - * ** Wait for Data Job delete req. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobDeleteReqInterceptor() { - return this.executeCypressCommand('waitForDataJobDeleteReqInterceptor', 1, 1000); - } - - /** - * ** Wait for Data Job execution delete req. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobExecutionDeleteReqInterceptor() { - return this.executeCypressCommand('waitForDataJobExecutionDeleteReqInterceptor', 1, 1000); - } - - /** - * ** Wait for Data Job executions get req. - * - * @return {Cypress.Chainable} - */ - static waitForGetExecutionsReqInterceptor() { - return cy.wait('@dataJobsExecutionsGetReq'); - } - - /** - * ** Wait for Data Job to not have running executions. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobToNotHaveRunningExecution() { - cy.log('Looking if there is ongoing running execution'); - - let isRunningExecution = false; - - return cy.waitForInterceptorWithRetry('@dataJobsExecutionsGetReq', 5, { - predicate: (interception) => { - if (interception?.response?.statusCode !== 200) { - return false; - } - - const body = interception?.response?.body; - if (body && body.data && body.data.content) { - if (body.data.content.length === 0 || body.data.content[0].__typename !== 'DataJobExecution') { - return false; - } - - isRunningExecution = - body.data.content.findIndex((e) => { - if (!e || e.status === undefined) { - return false; - } - - return e.status?.toLowerCase() === 'running' || e.status?.toLowerCase() === 'submitted'; - }) !== -1; - - return true; - } + /** + * ** Returns instance of the page object. + * + * @returns {DataJobBasePO} + */ + static getPage() { + return new DataJobBasePO(); + } + + /** + * ** Init Http requests interceptors. + */ + static initInterceptors() { + super.initInterceptors(); + + this.executeCypressCommand("initDataJobsSingleExecutionGetReqInterceptor"); + this.executeCypressCommand("initDataJobsExecutionsGetReqInterceptor"); + this.executeCypressCommand("initDataJobPutReqInterceptor"); + this.executeCypressCommand("initDataJobDeleteReqInterceptor"); + this.executeCypressCommand("initDataJobExecutionDeleteReqInterceptor"); + } + + /** + * ** Navigate to Data Job Url. + * + * @param {'explore'|'manage'} context + * @param {string} teamName + * @param {string} jobName + * @param {'details'|'executions'|'lineage'} subpage + */ + static navigateTo(context, teamName, jobName, subpage) { + const numberOfDataJobsApiGetReqInterceptorWaiting = + subpage === "details" ? 4 : 3; + + return this.navigateToDataJobUrl( + `/${context}/data-jobs/${teamName}/${jobName}/${subpage}`, + numberOfDataJobsApiGetReqInterceptorWaiting, + ); + } + + /** + * ** Wait for Data Job put req. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobPutReqInterceptor() { + return this.executeCypressCommand( + "waitForDataJobPutReqInterceptor", + 1, + 1000, + ); + } + + /** + * ** Wait for Data Job delete req. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobDeleteReqInterceptor() { + return this.executeCypressCommand( + "waitForDataJobDeleteReqInterceptor", + 1, + 1000, + ); + } + + /** + * ** Wait for Data Job execution delete req. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobExecutionDeleteReqInterceptor() { + return this.executeCypressCommand( + "waitForDataJobExecutionDeleteReqInterceptor", + 1, + 1000, + ); + } + + /** + * ** Wait for Data Job executions get req. + * + * @return {Cypress.Chainable} + */ + static waitForGetExecutionsReqInterceptor() { + return cy.wait("@dataJobsExecutionsGetReq"); + } + + /** + * ** Wait for Data Job to not have running executions. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobToNotHaveRunningExecution() { + cy.log("Looking if there is ongoing running execution"); + + let isRunningExecution = false; + + return cy.waitForInterceptorWithRetry("@dataJobsExecutionsGetReq", 5, { + predicate: (interception) => { + if (interception?.response?.statusCode !== 200) { + return false; + } + const body = interception?.response?.body; + if (body && body.data && body.data.content) { + if ( + body.data.content.length === 0 || + body.data.content[0].__typename !== "DataJobExecution" + ) { + return false; + } + + isRunningExecution = + body.data.content.findIndex((e) => { + if (!e || e.status === undefined) { return false; - }, - onfulfill: () => { - cy.log('Waiting for already running execution to stop'); - - if (isRunningExecution) { - DataJobBasePO.waitForDataJobToFinishExecution(); - } - } - }); - } + } - /** - * ** Wait for Data Job to finish execution. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobToFinishExecution() { - cy.log('Waiting execution to finish'); + return ( + e.status?.toLowerCase() === "running" || + e.status?.toLowerCase() === "submitted" + ); + }) !== -1; - return cy.waitForInterceptorWithRetry('@dataJobsSingleExecutionGetReq', 20, { - predicate: (interception) => { - const body = interception?.response?.body; - - return body?.status?.toLowerCase() !== 'running' && body?.status?.toLowerCase() !== 'submitted'; - } - }); - } - - /** - * ** Wait for Data Job execution to become canceled. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobToCancelExecution() { - cy.log('Waiting to cancel execution'); - - return cy.waitForInterceptorWithRetry('@dataJobsSingleExecutionGetReq', 20, { - predicate: (interception) => { - return interception?.response?.body?.status?.toLowerCase() === 'cancelled'; - } - }); - } - - /** - * ** Wait for response where there would be execution record with status SUBMITTED or RUNNING. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobStartExecute() { - cy.log('Waiting execution to start SUBMITTED|RUNNING'); - - return cy.waitForInterceptorWithRetry('@dataJobsExecutionsGetReq', 20, { - predicate: (interception) => { - if (interception?.response?.statusCode !== 200) { - return false; - } - - const body = interception?.response?.body; - if (body?.data?.content?.length === 0 || body?.data?.content[0]?.__typename !== 'DataJobExecution') { - return false; - } - - return ( - body?.data?.content?.findIndex((e) => { - if (!e || e.status === undefined) { - return false; - } - - return e.status?.toLowerCase() === 'running' || e.status?.toLowerCase() === 'submitted'; - }) !== -1 - ); - } - }); - } - - /** - * ** Wait for response where there would be execution record with status SUBMITTED or RUNNING. - * - * @return {Cypress.Chainable} - */ - static waitForPollingToShowRunningExecution() { - cy.log('Waiting for pooling execution with status RUNNING'); - - return cy.waitForInterceptorWithRetry('@dataJobsSingleExecutionGetReq', 10, { - predicate: (interception) => { - const body = interception?.response?.body; - const status = `${body?.status}`.toLowerCase(); - - if (status === 'running') { - return true; - } else { - if (status !== 'submitted') { - cy.log(`Execution status never changed to RUNNING, current is ${status.toUpperCase()}`); - - return true; - } - - return false; - } - } - }); - } - - /** - * ** Wait for Data Job put req. - * - * @return {Cypress.Chainable} - */ - waitForDataJobPutReqInterceptor() { - return DataJobBasePO.waitForDataJobPutReqInterceptor(); - } - - /** - * ** Wait for Data Job delete req. - * - * @return {Cypress.Chainable} - */ - waitForDataJobDeleteReqInterceptor() { - return DataJobBasePO.waitForDataJobDeleteReqInterceptor(); - } - - /** - * ** Wait for Data Job execution delete req. - * - * @return {Cypress.Chainable} - */ - waitForDataJobExecutionDeleteReqInterceptor() { - return DataJobBasePO.waitForDataJobExecutionDeleteReqInterceptor(); - } - - /** - * ** Wait for Data Job executions get req. - * - * @return {Cypress.Chainable} - */ - waitForGetExecutionsReqInterceptor() { - const waitChainable = DataJobBasePO.waitForGetExecutionsReqInterceptor(); - if (waitChainable) { - return waitChainable; + return true; } - return cy.wait(500).then(() => this.waitForGetExecutionsReqInterceptor()); - } - - /* Selectors */ - - getNavigateBackBtn() { - return cy.get('[data-cy=data-pipelines-job-navigate-back]'); - } + return false; + }, + onfulfill: () => { + cy.log("Waiting for already running execution to stop"); - getExecuteNowButton() { - return cy.get('[data-cy=data-pipelines-job-execute-btn]', { - timeout: DataJobBasePO.WAIT_SHORT_TASK - }); - } - - getCancelExecutionButton() { - return cy.get('[data-cy=data-pipelines-job-cancel-execution-btn]'); - } - - getExecuteOrCancelButton() { - return cy.get('[data-cy=data-pipelines-job-actions-container]').then(($container) => { - if ($container && $container.length > 0) { - const $btn = $container.find('[data-cy=data-pipelines-job-execute-btn]'); - - if ($btn && $btn.length > 0) { - return $btn; - } - } - - return $container.find('[data-cy=data-pipelines-job-cancel-execution-btn]'); - }); - } - - getConfirmDialogButton() { - return cy.get('[data-cy="confirmation-dialog-ok-btn"]'); - } - - getActionDropdownBtn() { - return cy.get('[data-cy=data-pipelines-job-action-dropdown-btn]'); - } - - getDownloadKeyBtn() { - return cy.get('[data-cy=data-pipelines-job-download-btn]'); - } - - getDeleteJobBtn() { - return cy.get('[data-cy=data-pipelines-job-delete-btn]'); - } - - getConfirmDeleteBtn() { - return cy.get('#removeBtn'); - } - - getDetailsTab() { - return cy.get('[data-cy=data-pipelines-job-details-tab]'); - } - - getExecutionsTab() { - return cy.get('[data-cy=data-pipelines-job-executions-tab]'); - } - - getExecutionStatus() { - return cy.get('[data-cy="data-pipelines-job-execution-status"]'); - } - - /* Actions */ - - executeNow(waitToStartExecution = false) { - this.getExecuteNowButton().should('exist').scrollIntoView().click({ force: true }); - - this.confirmInConfirmDialog(() => { - this.waitForDataJobExecutionPostReqInterceptor(); - }); - - if (waitToStartExecution) { - DataJobBasePO.waitForDataJobStartExecute(); - DataJobBasePO.waitForPollingToShowRunningExecution(); - this.waitForViewToRenderShort(); + if (isRunningExecution) { + DataJobBasePO.waitForDataJobToFinishExecution(); } - } - - cancelExecution(waitExecutionToStop = false) { - this.getCancelExecutionButton().should('exist').scrollIntoView().click({ force: true }); - - this.confirmInConfirmDialog(() => { - this.waitForDataJobExecutionDeleteReqInterceptor(); - }); - - if (waitExecutionToStop) { - DataJobBasePO.waitForDataJobToCancelExecution(); - this.waitForViewToRenderShort(); + }, + }); + } + + /** + * ** Wait for Data Job to finish execution. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobToFinishExecution() { + cy.log("Waiting execution to finish"); + + return cy.waitForInterceptorWithRetry( + "@dataJobsSingleExecutionGetReq", + 20, + { + predicate: (interception) => { + const body = interception?.response?.body; + + return ( + body?.status?.toLowerCase() !== "running" && + body?.status?.toLowerCase() !== "submitted" + ); + }, + }, + ); + } + + /** + * ** Wait for Data Job execution to become canceled. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobToCancelExecution() { + cy.log("Waiting to cancel execution"); + + return cy.waitForInterceptorWithRetry( + "@dataJobsSingleExecutionGetReq", + 20, + { + predicate: (interception) => { + return ( + interception?.response?.body?.status?.toLowerCase() === "cancelled" + ); + }, + }, + ); + } + + /** + * ** Wait for response where there would be execution record with status SUBMITTED or RUNNING. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobStartExecute() { + cy.log("Waiting execution to start SUBMITTED|RUNNING"); + + return cy.waitForInterceptorWithRetry("@dataJobsExecutionsGetReq", 20, { + predicate: (interception) => { + if (interception?.response?.statusCode !== 200) { + return false; } - } - - openActionDropdown() { - this.getActionDropdownBtn().should('exist').scrollIntoView().should('be.visible').click({ force: true }); - this.waitForViewToRenderShort(); - } + const body = interception?.response?.body; + if ( + body?.data?.content?.length === 0 || + body?.data?.content[0]?.__typename !== "DataJobExecution" + ) { + return false; + } - downloadJobKey() { - this.getDownloadKeyBtn().should('exist').scrollIntoView().should('be.visible').click({ force: true }); + return ( + body?.data?.content?.findIndex((e) => { + if (!e || e.status === undefined) { + return false; + } - this.waitForDataJobsApiGetReqInterceptor(); - } + return ( + e.status?.toLowerCase() === "running" || + e.status?.toLowerCase() === "submitted" + ); + }) !== -1 + ); + }, + }); + } + + /** + * ** Wait for response where there would be execution record with status SUBMITTED or RUNNING. + * + * @return {Cypress.Chainable} + */ + static waitForPollingToShowRunningExecution() { + cy.log("Waiting for pooling execution with status RUNNING"); + + return cy.waitForInterceptorWithRetry( + "@dataJobsSingleExecutionGetReq", + 10, + { + predicate: (interception) => { + const body = interception?.response?.body; + const status = `${body?.status}`.toLowerCase(); + + if (status === "running") { + return true; + } else { + if (status !== "submitted") { + cy.log( + `Execution status never changed to RUNNING, current is ${status.toUpperCase()}`, + ); + + return true; + } - deleteJob() { - this.getDeleteJobBtn().scrollIntoView().should('be.visible').click({ force: true }); + return false; + } + }, + }, + ); + } + + /** + * ** Wait for Data Job put req. + * + * @return {Cypress.Chainable} + */ + waitForDataJobPutReqInterceptor() { + return DataJobBasePO.waitForDataJobPutReqInterceptor(); + } + + /** + * ** Wait for Data Job delete req. + * + * @return {Cypress.Chainable} + */ + waitForDataJobDeleteReqInterceptor() { + return DataJobBasePO.waitForDataJobDeleteReqInterceptor(); + } + + /** + * ** Wait for Data Job execution delete req. + * + * @return {Cypress.Chainable} + */ + waitForDataJobExecutionDeleteReqInterceptor() { + return DataJobBasePO.waitForDataJobExecutionDeleteReqInterceptor(); + } + + /** + * ** Wait for Data Job executions get req. + * + * @return {Cypress.Chainable} + */ + waitForGetExecutionsReqInterceptor() { + const waitChainable = DataJobBasePO.waitForGetExecutionsReqInterceptor(); + if (waitChainable) { + return waitChainable; + } + + return cy.wait(500).then(() => this.waitForGetExecutionsReqInterceptor()); + } + + /* Selectors */ + + getNavigateBackBtn() { + return cy.get("[data-cy=data-pipelines-job-navigate-back]"); + } + + getExecuteNowButton() { + return cy.get("[data-cy=data-pipelines-job-execute-btn]", { + timeout: DataJobBasePO.WAIT_SHORT_TASK, + }); + } + + getCancelExecutionButton() { + return cy.get("[data-cy=data-pipelines-job-cancel-execution-btn]"); + } + + getExecuteOrCancelButton() { + return cy + .get("[data-cy=data-pipelines-job-actions-container]") + .then(($container) => { + if ($container && $container.length > 0) { + const $btn = $container.find( + "[data-cy=data-pipelines-job-execute-btn]", + ); + + if ($btn && $btn.length > 0) { + return $btn; + } + } - this.waitForViewToRenderShort(); + return $container.find( + "[data-cy=data-pipelines-job-cancel-execution-btn]", + ); + }); + } - this.confirmDeleteJob(() => { - this.waitForDataJobDeleteReqInterceptor(); - }); - } + getConfirmDialogButton() { + return cy.get('[data-cy="confirmation-dialog-ok-btn"]'); + } + + getActionDropdownBtn() { + return cy.get("[data-cy=data-pipelines-job-action-dropdown-btn]"); + } + + getDownloadKeyBtn() { + return cy.get("[data-cy=data-pipelines-job-download-btn]"); + } + + getDeleteJobBtn() { + return cy.get("[data-cy=data-pipelines-job-delete-btn]"); + } + + getConfirmDeleteBtn() { + return cy.get("#removeBtn"); + } + + getDetailsTab() { + return cy.get("[data-cy=data-pipelines-job-details-tab]"); + } + + getExecutionsTab() { + return cy.get("[data-cy=data-pipelines-job-executions-tab]"); + } - /** - * ** Confirm Job deletion. - * - * @param {() => void} interceptor - */ - confirmDeleteJob(interceptor) { - this.getConfirmDeleteBtn().scrollIntoView().should('be.visible').click({ force: true }); + getExecutionStatus() { + return cy.get('[data-cy="data-pipelines-job-execution-status"]'); + } - if (interceptor) { - interceptor(); - } - } + /* Actions */ + + executeNow(waitToStartExecution = false) { + this.getExecuteNowButton() + .should("exist") + .scrollIntoView() + .click({ force: true }); + + this.confirmInConfirmDialog(() => { + this.waitForDataJobExecutionPostReqInterceptor(); + }); + + if (waitToStartExecution) { + DataJobBasePO.waitForDataJobStartExecute(); + DataJobBasePO.waitForPollingToShowRunningExecution(); + this.waitForViewToRenderShort(); + } + } + + cancelExecution(waitExecutionToStop = false) { + this.getCancelExecutionButton() + .should("exist") + .scrollIntoView() + .click({ force: true }); + + this.confirmInConfirmDialog(() => { + this.waitForDataJobExecutionDeleteReqInterceptor(); + }); + + if (waitExecutionToStop) { + DataJobBasePO.waitForDataJobToCancelExecution(); + this.waitForViewToRenderShort(); + } + } + + openActionDropdown() { + this.getActionDropdownBtn() + .should("exist") + .scrollIntoView() + .should("be.visible") + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + downloadJobKey() { + this.getDownloadKeyBtn() + .should("exist") + .scrollIntoView() + .should("be.visible") + .click({ force: true }); + + this.waitForDataJobsApiGetReqInterceptor(); + } + + deleteJob() { + this.getDeleteJobBtn() + .scrollIntoView() + .should("be.visible") + .click({ force: true }); + + this.waitForViewToRenderShort(); + + this.confirmDeleteJob(() => { + this.waitForDataJobDeleteReqInterceptor(); + }); + } - navigateBackToDataJobs() { - this.getNavigateBackBtn().should('be.visible').click({ force: true }); + /** + * ** Confirm Job deletion. + * + * @param {() => void} interceptor + */ + confirmDeleteJob(interceptor) { + this.getConfirmDeleteBtn() + .scrollIntoView() + .should("be.visible") + .click({ force: true }); - this.waitForGridDataLoad(); - } + if (interceptor) { + interceptor(); + } + } - openDetailsTab() { - this.getDetailsTab().should('exist').click({ force: true }); + navigateBackToDataJobs() { + this.getNavigateBackBtn().should("be.visible").click({ force: true }); - this.waitForDataJobsApiGetReqInterceptor(); - } + this.waitForGridDataLoad(); + } + + openDetailsTab() { + this.getDetailsTab().should("exist").click({ force: true }); + + this.waitForDataJobsApiGetReqInterceptor(); + } - openExecutionsTab() { - this.getExecutionsTab().should('exist').click({ force: true }); + openExecutionsTab() { + this.getExecutionsTab().should("exist").click({ force: true }); - this.waitForGridDataLoad(); - } + this.waitForGridDataLoad(); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-details-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-details-base.po.js index 407a856e35..ec24e0f909 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-details-base.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-job-details-base.po.js @@ -5,82 +5,82 @@ /// -import { DataJobBasePO } from './data-job-base.po'; +import { DataJobBasePO } from "./data-job-base.po"; export class DataJobDetailsBasePO extends DataJobBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobDetailsBasePO} - */ - static getPage() { - return new DataJobDetailsBasePO(); - } - - /** - * ** Navigate to Data Job Url. - * - * @param {'explore'|'manage'} context - * @param {string} teamName - * @param {string} jobName - */ - static navigateTo(context, teamName, jobName) { - return super.navigateTo(context, teamName, jobName, 'details'); - } - - // Selectors - - getStatusField() { - return cy.get('[data-cy=data-pipelines-job-details-status]'); - } - - getDescriptionField() { - return cy.get('[data-cy=data-pipelines-job-details-description]'); - } - - getDescriptionFull() { - return cy.get('[data-cy=description-show-less]'); - } - - getShowDescriptionMoreBtn() { - return cy.get('[data-cy=description-show-more]'); - } - - getTeamField() { - return cy.get('[data-cy=data-pipelines-job-details-team]'); - } - - getScheduleField() { - return cy.get('[data-cy=data-pipelines-job-details-schedule]'); - } - - getSourceField() { - return cy.get('[data-cy=data-pipelines-job-details-source]'); - } - - getOnDeployedField() { - return cy.get('[data-cy=data-pipelines-job-details-on-deployed]'); - } - - getOnPlatformErrorField() { - return cy.get('[data-cy=data-pipelines-job-details-on-platform-error]'); - } - - getOnUserErrorField() { - return cy.get('[data-cy=data-pipelines-job-details-on-user-error]'); - } - - getOnSuccessField() { - return cy.get('[data-cy=data-pipelines-job-details-on-success]'); - } - - // Actions - - showMoreDescription() { - this.getShowDescriptionMoreBtn().should('exist').click({ force: true }); - - this.waitForViewToRenderShort(); - - return this; - } + /** + * ** Returns instance of the page object. + * + * @returns {DataJobDetailsBasePO} + */ + static getPage() { + return new DataJobDetailsBasePO(); + } + + /** + * ** Navigate to Data Job Url. + * + * @param {'explore'|'manage'} context + * @param {string} teamName + * @param {string} jobName + */ + static navigateTo(context, teamName, jobName) { + return super.navigateTo(context, teamName, jobName, "details"); + } + + // Selectors + + getStatusField() { + return cy.get("[data-cy=data-pipelines-job-details-status]"); + } + + getDescriptionField() { + return cy.get("[data-cy=data-pipelines-job-details-description]"); + } + + getDescriptionFull() { + return cy.get("[data-cy=description-show-less]"); + } + + getShowDescriptionMoreBtn() { + return cy.get("[data-cy=description-show-more]"); + } + + getTeamField() { + return cy.get("[data-cy=data-pipelines-job-details-team]"); + } + + getScheduleField() { + return cy.get("[data-cy=data-pipelines-job-details-schedule]"); + } + + getSourceField() { + return cy.get("[data-cy=data-pipelines-job-details-source]"); + } + + getOnDeployedField() { + return cy.get("[data-cy=data-pipelines-job-details-on-deployed]"); + } + + getOnPlatformErrorField() { + return cy.get("[data-cy=data-pipelines-job-details-on-platform-error]"); + } + + getOnUserErrorField() { + return cy.get("[data-cy=data-pipelines-job-details-on-user-error]"); + } + + getOnSuccessField() { + return cy.get("[data-cy=data-pipelines-job-details-on-success]"); + } + + // Actions + + showMoreDescription() { + this.getShowDescriptionMoreBtn().should("exist").click({ force: true }); + + this.waitForViewToRenderShort(); + + return this; + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-jobs-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-jobs-base.po.js index d1e5e06f6b..2b2e1fa8de 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-jobs-base.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-jobs-base.po.js @@ -5,327 +5,366 @@ /// -import { DataPipelinesBasePO } from './data-pipelines-base.po'; +import { DataPipelinesBasePO } from "./data-pipelines-base.po"; export class DataJobsBasePO extends DataPipelinesBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobsBasePO} - */ - static getPage() { - return new DataJobsBasePO(); - } - - /** - * ** Navigate to Data Job Url. - * - * @param {'explore'|'manage'} context - */ - static navigateTo(context) { - return this.navigateToDataJobUrl(`/${context}/data-jobs`); - } - - /** - * ** Navigate to Data Job Url. - * ** Do not wait for bootstrap and interceptors - * @param {'explore'|'manage'} context - */ - static navigateToNoBootstrap(context) { - return this.navigateToDataJobUrlNoBootstrap(`/${context}/data-jobs`); - } - // Selectors - - getDataGridRefreshButton() { - throw new Error('Method should be overridden by subclasses'); - } - - getDataGrid() { - return cy.get('clr-datagrid'); - } - - getDataGridSearchInput() { - return cy.get('[data-test-id=search-input]').first(); - } - - getDataGridClearSearchButton() { - return cy.get('[data-test-id="clear-search-btn"]').first(); - } - - getQuickFilters() { - return cy.get('[data-cy=data-pipelines-quick-filters] > span'); - } - - getHeaderColumnJobName() { - return cy.get('[data-cy=data-pipelines-jobs-name-column]'); - } - - getHeaderColumnTeamName() { - return cy.get('[data-cy=data-pipelines-jobs-team-column]'); - } - - getHeaderColumnDescriptionName() { - return cy.get('[data-cy=data-pipelines-jobs-description-column]'); - } - - getDataGridJobNameFilter() { - return this.getHeaderColumnJobName().should('exist').find('clr-dg-filter button'); - } - - getHeaderColumnJobNameSortBtn() { - return this.getHeaderColumnJobName().should('exist').find('.datagrid-column-title'); - } - - getDataGridJobTeamFilter() { - return this.getHeaderColumnTeamName().should('exist').find('clr-dg-filter button'); - } - - getHeaderColumnJobTeamSortBtn() { - return this.getHeaderColumnTeamName().should('exist').find('.datagrid-column-title'); - } - - getDataGridJobDescriptionFilter() { - return this.getHeaderColumnDescriptionName().should('exist').find('clr-dg-filter button'); - } - - getDataGridFilterInput() { - return cy.get('div.datagrid-filter input'); - } - - getDataGridHeaderCell(content) { - return this.getDataGrid() - .should('exist') - .find('clr-dg-column:not(.datagrid-hidden-column)') - .contains(new RegExp(`${content}`)); - } - - // Rows and Cells - - getDataGridRows() { - return this.getDataGrid().should('exist').find('clr-dg-row.datagrid-row'); - } - - getDataGridRowByIndex(rowIndex) { - return this.getDataGridRows() - .should('have.length.gte', rowIndex - 1) - .then((rows) => Array.from(rows)[rowIndex - 1]); - } - - getDataGridRowByName(jobName) { - return this.getDataGridCell(jobName).parents('clr-dg-row'); - } - - getDataGridCells(rowIndex) { - return this.getDataGridRowByIndex(rowIndex).should('exist').find('clr-dg-cell.datagrid-cell'); - } - - getDataGridCellByIndex(rowIndex, cellIndex) { - return this.getDataGridCells(rowIndex) - .should('have.length.gte', cellIndex - 1) - .then((cells) => Array.from(cells)[cellIndex - 1]); - } - - getDataGridCellByIdentifier(rowIndex, identifier) { - return this.getDataGridRowByIndex(rowIndex).should('exist').find(identifier); - } - - getDataGridCell(content, timeout) { - return cy.get('[id^="clr-dg-row"] > .datagrid-row-scrollable > .datagrid-scrolling-cells > .ng-star-inserted').contains(new RegExp(`^\\s*${content}\\s*$`), { - timeout: this.resolveTimeout(timeout) - }); - } - - getDataGridStatusCells() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-manage-grid-status-cell]'); - } - - getDataGridStatusIcons() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-manage-grid-status-cell] clr-icon[data-cy*=data-pipelines-job]'); - } - - getDataGridColumnToggle() { - return this.getDataGrid().should('exist').find('clr-dg-column-toggle button'); - } + /** + * ** Returns instance of the page object. + * + * @returns {DataJobsBasePO} + */ + static getPage() { + return new DataJobsBasePO(); + } + + /** + * ** Navigate to Data Job Url. + * + * @param {'explore'|'manage'} context + */ + static navigateTo(context) { + return this.navigateToDataJobUrl(`/${context}/data-jobs`); + } + + /** + * ** Navigate to Data Job Url. + * ** Do not wait for bootstrap and interceptors + * @param {'explore'|'manage'} context + */ + static navigateToNoBootstrap(context) { + return this.navigateToDataJobUrlNoBootstrap(`/${context}/data-jobs`); + } + // Selectors + + getDataGridRefreshButton() { + throw new Error("Method should be overridden by subclasses"); + } + + getDataGrid() { + return cy.get("clr-datagrid"); + } + + getDataGridSearchInput() { + return cy.get("[data-test-id=search-input]").first(); + } + + getDataGridClearSearchButton() { + return cy.get('[data-test-id="clear-search-btn"]').first(); + } + + getQuickFilters() { + return cy.get("[data-cy=data-pipelines-quick-filters] > span"); + } + + getHeaderColumnJobName() { + return cy.get("[data-cy=data-pipelines-jobs-name-column]"); + } + + getHeaderColumnTeamName() { + return cy.get("[data-cy=data-pipelines-jobs-team-column]"); + } + + getHeaderColumnDescriptionName() { + return cy.get("[data-cy=data-pipelines-jobs-description-column]"); + } + + getDataGridJobNameFilter() { + return this.getHeaderColumnJobName() + .should("exist") + .find("clr-dg-filter button"); + } + + getHeaderColumnJobNameSortBtn() { + return this.getHeaderColumnJobName() + .should("exist") + .find(".datagrid-column-title"); + } + + getDataGridJobTeamFilter() { + return this.getHeaderColumnTeamName() + .should("exist") + .find("clr-dg-filter button"); + } + + getHeaderColumnJobTeamSortBtn() { + return this.getHeaderColumnTeamName() + .should("exist") + .find(".datagrid-column-title"); + } + + getDataGridJobDescriptionFilter() { + return this.getHeaderColumnDescriptionName() + .should("exist") + .find("clr-dg-filter button"); + } + + getDataGridFilterInput() { + return cy.get("div.datagrid-filter input"); + } + + getDataGridHeaderCell(content) { + return this.getDataGrid() + .should("exist") + .find("clr-dg-column:not(.datagrid-hidden-column)") + .contains(new RegExp(`${content}`)); + } + + // Rows and Cells + + getDataGridRows() { + return this.getDataGrid().should("exist").find("clr-dg-row.datagrid-row"); + } + + getDataGridRowByIndex(rowIndex) { + return this.getDataGridRows() + .should("have.length.gte", rowIndex - 1) + .then((rows) => Array.from(rows)[rowIndex - 1]); + } + + getDataGridRowByName(jobName) { + return this.getDataGridCell(jobName).parents("clr-dg-row"); + } + + getDataGridCells(rowIndex) { + return this.getDataGridRowByIndex(rowIndex) + .should("exist") + .find("clr-dg-cell.datagrid-cell"); + } + + getDataGridCellByIndex(rowIndex, cellIndex) { + return this.getDataGridCells(rowIndex) + .should("have.length.gte", cellIndex - 1) + .then((cells) => Array.from(cells)[cellIndex - 1]); + } + + getDataGridCellByIdentifier(rowIndex, identifier) { + return this.getDataGridRowByIndex(rowIndex) + .should("exist") + .find(identifier); + } + + getDataGridCell(content, timeout) { + return cy + .get( + '[id^="clr-dg-row"] > .datagrid-row-scrollable > .datagrid-scrolling-cells > .ng-star-inserted', + ) + .contains(new RegExp(`^\\s*${content}\\s*$`), { + timeout: this.resolveTimeout(timeout), + }); + } + + getDataGridStatusCells() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-manage-grid-status-cell]"); + } + + getDataGridStatusIcons() { + return this.getDataGrid() + .should("exist") + .find( + "[data-cy=data-pipelines-manage-grid-status-cell] clr-icon[data-cy*=data-pipelines-job]", + ); + } + + getDataGridColumnToggle() { + return this.getDataGrid() + .should("exist") + .find("clr-dg-column-toggle button"); + } + + getDataGridNavigateBtn(team, job) { + throw new Error("Method should be overridden by subclasses"); + } + + getDataGridColumnShowHidePanel() { + return cy.get(".column-switch"); + } + + getDataGridColumnShowHideOptions() { + return this.getDataGridColumnShowHidePanel() + .should("exist") + .find("clr-checkbox-wrapper"); + } + + getDataGridColumnShowHideOptionsValues() { + return this.getDataGridColumnShowHidePanel() + .should("exist") + .find("clr-checkbox-wrapper") + .then((elements) => Array.from(elements).map((el) => el.innerText)); + } + + getDataGridColumnShowHideOption(option) { + return this.getDataGridColumnShowHideOptions() + .should("have.length.gt", 0) + .then((elements) => + Array.from(elements).find((el) => el.innerText === option), + ) + .find("input"); + } + + /** + * ** Get DataGrid page size selection. + * + * @returns {Cypress.Chainable>} + */ + getDataGridPageSizeSelect() { + return this.getDataGrid().find("clr-dg-page-size .clr-page-size-select"); + } + + getDataJobPage() { + return cy.get("[data-cy=data-pipelines-job-page]"); + } + + // Actions + + chooseQuickFilter(filterPosition) { + this.getQuickFilters() + .then((filters) => { + return filters && filters[filterPosition]; + }) + .should("exist") + .click({ force: true }); + + this.waitForGridDataLoad(); + } - getDataGridNavigateBtn(team, job) { - throw new Error('Method should be overridden by subclasses'); - } + refreshDataGrid() { + this.getDataGridRefreshButton().should("exist").click({ force: true }); - getDataGridColumnShowHidePanel() { - return cy.get('.column-switch'); - } + this.waitForGridDataLoad(); + } - getDataGridColumnShowHideOptions() { - return this.getDataGridColumnShowHidePanel().should('exist').find('clr-checkbox-wrapper'); - } + /** + * ** Search by job name. + * + * @param {string} jobName + */ + searchByJobName(jobName) { + this.getDataGridSearchInput().should("exist").type(jobName); - getDataGridColumnShowHideOptionsValues() { - return this.getDataGridColumnShowHidePanel() - .should('exist') - .find('clr-checkbox-wrapper') - .then((elements) => Array.from(elements).map((el) => el.innerText)); - } + this.waitForGridDataLoad(); + } - getDataGridColumnShowHideOption(option) { - return this.getDataGridColumnShowHideOptions() - .should('have.length.gt', 0) - .then((elements) => Array.from(elements).find((el) => el.innerText === option)) - .find('input'); - } + clearSearchField() { + this.getDataGridSearchInput().should("exist").clear(); - /** - * ** Get DataGrid page size selection. - * - * @returns {Cypress.Chainable>} - */ - getDataGridPageSizeSelect() { - return this.getDataGrid().find('clr-dg-page-size .clr-page-size-select'); - } + this.waitForGridDataLoad(); + } - getDataJobPage() { - return cy.get('[data-cy=data-pipelines-job-page]'); - } + clearSearchFieldWithButton() { + this.getDataGridClearSearchButton().should("exist").click({ force: true }); - // Actions + this.waitForGridDataLoad(); + } - chooseQuickFilter(filterPosition) { - this.getQuickFilters() - .then((filters) => { - return filters && filters[filterPosition]; - }) - .should('exist') - .click({ force: true }); + filterByJobName(jobName) { + this.getDataGridJobNameFilter().click({ force: true }); - this.waitForGridDataLoad(); - } + this.getDataGridFilterInput().should("be.visible").type(jobName); - refreshDataGrid() { - this.getDataGridRefreshButton().should('exist').click({ force: true }); + this.getContentContainer().should("be.visible").click({ force: true }); - this.waitForGridDataLoad(); - } + this.waitForGridDataLoad(); - /** - * ** Search by job name. - * - * @param {string} jobName - */ - searchByJobName(jobName) { - this.getDataGridSearchInput().should('exist').type(jobName); + this.waitForViewToRenderShort(); + } - this.waitForGridDataLoad(); - } + filterByJobDescription(description) { + this.getDataGridJobDescriptionFilter().click({ force: true }); - clearSearchField() { - this.getDataGridSearchInput().should('exist').clear(); + this.getDataGridFilterInput().should("be.visible").type(description); - this.waitForGridDataLoad(); - } + this.getContentContainer().should("be.visible").click({ force: true }); - clearSearchFieldWithButton() { - this.getDataGridClearSearchButton().should('exist').click({ force: true }); + this.waitForGridDataLoad(); - this.waitForGridDataLoad(); - } + this.waitForViewToRenderShort(); + } - filterByJobName(jobName) { - this.getDataGridJobNameFilter().click({ force: true }); + sortByJobName() { + this.getHeaderColumnJobNameSortBtn().should("exist").click({ force: true }); - this.getDataGridFilterInput().should('be.visible').type(jobName); + this.waitForGridDataLoad(); + } - this.getContentContainer().should('be.visible').click({ force: true }); + filterByJobTeamName(teamName) { + this.getDataGridJobTeamFilter().click({ force: true }); - this.waitForGridDataLoad(); + this.getDataGridFilterInput().should("be.visible").type(teamName); - this.waitForViewToRenderShort(); - } + this.getContentContainer().should("be.visible").click({ force: true }); - filterByJobDescription(description) { - this.getDataGridJobDescriptionFilter().click({ force: true }); + this.waitForGridDataLoad(); + } - this.getDataGridFilterInput().should('be.visible').type(description); + sortByJobTeamName() { + this.getHeaderColumnJobTeamSortBtn().should("exist").click({ force: true }); - this.getContentContainer().should('be.visible').click({ force: true }); + this.waitForGridDataLoad(); + } - this.waitForGridDataLoad(); + selectRow(jobName) { + this.getDataGridRowByName(jobName) + .find(".datagrid-select input") + .check({ force: true }); - this.waitForViewToRenderShort(); - } + this.waitForViewToRenderShort(); + } - sortByJobName() { - this.getHeaderColumnJobNameSortBtn().should('exist').click({ force: true }); + openJobDetails(teamName, jobName) { + this.getDataGridNavigateBtn(teamName, jobName) + .scrollIntoView() + .should("exist") + .click({ force: true }); - this.waitForGridDataLoad(); - } + this.waitForDataJobsApiGetReqInterceptor(4); - filterByJobTeamName(teamName) { - this.getDataGridJobTeamFilter().click({ force: true }); + this.waitForViewToRender(); - this.getDataGridFilterInput().should('be.visible').type(teamName); + this.getDataJobPage().should("be.visible"); + } - this.getContentContainer().should('be.visible').click({ force: true }); + toggleColumnShowHidePanel() { + this.getDataGridColumnToggle().should("exist").click({ force: true }); - this.waitForGridDataLoad(); - } + this.waitForClickThinkingTime(); + } - sortByJobTeamName() { - this.getHeaderColumnJobTeamSortBtn().should('exist').click({ force: true }); + checkColumnShowHideOption(option) { + this.getDataGridColumnShowHideOption(option) + .should("exist") + .check({ force: true }); - this.waitForGridDataLoad(); - } + this.waitForViewToRenderShort(); + } - selectRow(jobName) { - this.getDataGridRowByName(jobName).find('.datagrid-select input').check({ force: true }); + uncheckColumnShowHideOption(option) { + this.getDataGridColumnShowHideOption(option) + .should("exist") + .uncheck({ force: true }); - this.waitForViewToRenderShort(); - } + this.waitForViewToRenderShort(); + } - openJobDetails(teamName, jobName) { - this.getDataGridNavigateBtn(teamName, jobName).scrollIntoView().should('exist').click({ force: true }); + /** + * ** Change DataGrid page size + * + * - hint options in Clarity are generated with values e.g. + * - '0: 25' + * - '1: 50' + * - '2: 100' + * + * @param {string} size + */ + changePageSize(size) { + this.getDataGridPageSizeSelect() + .should("be.visible") + .then(($element) => { + return cy.wait(1000).then(() => $element); + }) + .select(size); - this.waitForDataJobsApiGetReqInterceptor(4); - - this.waitForViewToRender(); - - this.getDataJobPage().should('be.visible'); - } - - toggleColumnShowHidePanel() { - this.getDataGridColumnToggle().should('exist').click({ force: true }); - - this.waitForClickThinkingTime(); - } - - checkColumnShowHideOption(option) { - this.getDataGridColumnShowHideOption(option).should('exist').check({ force: true }); - - this.waitForViewToRenderShort(); - } - - uncheckColumnShowHideOption(option) { - this.getDataGridColumnShowHideOption(option).should('exist').uncheck({ force: true }); - - this.waitForViewToRenderShort(); - } - - /** - * ** Change DataGrid page size - * - * - hint options in Clarity are generated with values e.g. - * - '0: 25' - * - '1: 50' - * - '2: 100' - * - * @param {string} size - */ - changePageSize(size) { - this.getDataGridPageSizeSelect() - .should('be.visible') - .then(($element) => { - return cy.wait(1000).then(() => $element); - }) - .select(size); - - this.waitForGridDataLoad(); - } + this.waitForGridDataLoad(); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-pipelines-base.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-pipelines-base.po.js index f18bca99e6..f8bd06788a 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-pipelines-base.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/base/data-pipelines/data-pipelines-base.po.js @@ -5,502 +5,543 @@ /// -import { TEAM_VDK, TEAM_VDK_DATA_JOB_FAILING, TEAM_VDK_DATA_JOB_TEST_V10, TEAM_VDK_DATA_JOB_TEST_V11, TEAM_VDK_DATA_JOB_TEST_V12, TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0, TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1, TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2 } from '../../../helpers/constants.support'; +import { + TEAM_VDK, + TEAM_VDK_DATA_JOB_FAILING, + TEAM_VDK_DATA_JOB_TEST_V10, + TEAM_VDK_DATA_JOB_TEST_V11, + TEAM_VDK_DATA_JOB_TEST_V12, + TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0, + TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1, + TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2, +} from "../../../helpers/constants.support"; -import { applyGlobalEnvSettings } from '../../../../plugins/helpers/util-helpers.plugins'; +import { applyGlobalEnvSettings } from "../../../../plugins/helpers/util-helpers.plugins"; -import { BasePagePO } from '../base-page.po'; +import { BasePagePO } from "../base-page.po"; export class DataPipelinesBasePO extends BasePagePO { - // Generics - - /** - * ** Returns instance of the page object. - * - * @returns {DataPipelinesBasePO} - */ - static getPage() { - return new DataPipelinesBasePO(); + // Generics + + /** + * ** Returns instance of the page object. + * + * @returns {DataPipelinesBasePO} + */ + static getPage() { + return new DataPipelinesBasePO(); + } + + /** + * ** Init Http requests interceptors. + */ + static initInterceptors() { + super.initInterceptors(); + + this.executeCypressCommand("initDataJobDeploymentPatchReqInterceptor"); + this.executeCypressCommand("initDataJobExecutionPostReqInterceptor"); + } + + /** + * ** Navigate to some url. + * + * @param {string} url + * @param {number} numberOfDataJobsApiGetReqInterceptorWaiting + */ + static navigateToDataJobUrl( + url, + numberOfDataJobsApiGetReqInterceptorWaiting = 1, + ) { + this.navigateToUrl(url); + + this.waitForApplicationBootstrap(); + this.waitForDataJobsApiGetReqInterceptor( + numberOfDataJobsApiGetReqInterceptorWaiting, + ); + + return this.getPage(); + } + + /** + * ** Navigate to some url without bootstrap. + * + * @param {string} url + */ + static navigateToDataJobUrlNoBootstrap(url) { + this.navigateToUrl(url); + return this.getPage(); + } + + /** + * ** Wait for Data Job post execution req. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobExecutionPostReqInterceptor() { + return this.executeCypressCommand( + "waitForDataJobExecutionPostReqInterceptor", + ); + } + + /** + * ** Wait for Data Job patch deployment req. + * + * @return {Cypress.Chainable} + */ + static waitForDataJobDeploymentPatchReqInterceptor() { + return this.executeCypressCommand( + "waitForDataJobDeploymentPatchReqInterceptor", + ); + } + + // Plugins invoking + + /** + * ** Create long-lived Data Jobs. + * + * @param {'failing'} instruction + * @returns {Cypress.Chainable} + */ + static createLongLivedJobs(...instruction) { + const relativePathToFixtures = []; + if (instruction.includes("failing")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`, + pathToZipFile: `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.zip`, + }); } - /** - * ** Init Http requests interceptors. - */ - static initInterceptors() { - super.initInterceptors(); - - this.executeCypressCommand('initDataJobDeploymentPatchReqInterceptor'); - this.executeCypressCommand('initDataJobExecutionPostReqInterceptor'); + return cy.task( + "createDeployJobs", + { + relativePathToFixtures, + }, + { timeout: this.WAIT_EXTRA_LONG_TASK }, + ); + } + + /** + * ** Create base short-lived test job with deployment. + * + * - They are created during tests execution and are deleted before and after test suits. + * - Executed in context of test environment. + * + * @param {'v0'|'v1'|'v2'} jobVersion + * @returns {Cypress.Chainable} + */ + static createShortLivedTestJobWithDeploy(...jobVersion) { + const relativePathToFixtures = []; + if (jobVersion.includes("v0")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.json`, + pathToZipFile: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.zip`, + }); } - /** - * ** Navigate to some url. - * - * @param {string} url - * @param {number} numberOfDataJobsApiGetReqInterceptorWaiting - */ - static navigateToDataJobUrl(url, numberOfDataJobsApiGetReqInterceptorWaiting = 1) { - this.navigateToUrl(url); - - this.waitForApplicationBootstrap(); - this.waitForDataJobsApiGetReqInterceptor(numberOfDataJobsApiGetReqInterceptorWaiting); - - return this.getPage(); + if (jobVersion.includes("v1")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.json`, + pathToZipFile: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.zip`, + }); } - /** - * ** Navigate to some url without bootstrap. - * - * @param {string} url - */ - static navigateToDataJobUrlNoBootstrap(url) { - this.navigateToUrl(url); - return this.getPage(); + if (jobVersion.includes("v2")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.json`, + pathToZipFile: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.zip`, + }); } - /** - * ** Wait for Data Job post execution req. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobExecutionPostReqInterceptor() { - return this.executeCypressCommand('waitForDataJobExecutionPostReqInterceptor'); + return cy.task( + "createDeployJobs", + { + relativePathToFixtures, + }, + { timeout: this.WAIT_EXTRA_LONG_TASK }, + ); + } + + /** + * ** Provide executions for long-lived Data Jobs. + * + * @param {{job: 'failing'; executions?: number;}} instruction + * @returns {Cypress.Chainable} + */ + static provideExecutionsForLongLivedJobs(...instruction) { + const relativePathToFixtures = []; + const foundFailingIndex = instruction.findIndex((i) => i.job === "failing"); + if (foundFailingIndex !== -1) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`, + executions: instruction[foundFailingIndex]?.executions ?? 2, + }); } - /** - * ** Wait for Data Job patch deployment req. - * - * @return {Cypress.Chainable} - */ - static waitForDataJobDeploymentPatchReqInterceptor() { - return this.executeCypressCommand('waitForDataJobDeploymentPatchReqInterceptor'); + return cy.task( + "provideDataJobsExecutions", + { + relativePathToFixtures, + }, + { timeout: DataPipelinesBasePO.WAIT_EXTRA_LONG_TASK }, + ); + } + + /** + * ** Provide one execution for short-lived Data Job with deployment. + * + * @param {'v0'|'v1'|'v2'} jobVersion + * @returns {Cypress.Chainable} + */ + static provideExecutionsForShortLivedTestJobWithDeploy(...jobVersion) { + const relativePathToFixtures = []; + if (jobVersion.includes("v0")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.json`, + executions: 1, + }); } - // Plugins invoking - - /** - * ** Create long-lived Data Jobs. - * - * @param {'failing'} instruction - * @returns {Cypress.Chainable} - */ - static createLongLivedJobs(...instruction) { - const relativePathToFixtures = []; - if (instruction.includes('failing')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`, - pathToZipFile: `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.zip` - }); - } - - return cy.task( - 'createDeployJobs', - { - relativePathToFixtures - }, - { timeout: this.WAIT_EXTRA_LONG_TASK } - ); + if (jobVersion.includes("v1")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.json`, + executions: 1, + }); } - /** - * ** Create base short-lived test job with deployment. - * - * - They are created during tests execution and are deleted before and after test suits. - * - Executed in context of test environment. - * - * @param {'v0'|'v1'|'v2'} jobVersion - * @returns {Cypress.Chainable} - */ - static createShortLivedTestJobWithDeploy(...jobVersion) { - const relativePathToFixtures = []; - if (jobVersion.includes('v0')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.json`, - pathToZipFile: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.zip` - }); - } - - if (jobVersion.includes('v1')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.json`, - pathToZipFile: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.zip` - }); - } - - if (jobVersion.includes('v2')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.json`, - pathToZipFile: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.zip` - }); - } - - return cy.task( - 'createDeployJobs', - { - relativePathToFixtures - }, - { timeout: this.WAIT_EXTRA_LONG_TASK } - ); + if (jobVersion.includes("v2")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.json`, + executions: 1, + }); } - /** - * ** Provide executions for long-lived Data Jobs. - * - * @param {{job: 'failing'; executions?: number;}} instruction - * @returns {Cypress.Chainable} - */ - static provideExecutionsForLongLivedJobs(...instruction) { - const relativePathToFixtures = []; - const foundFailingIndex = instruction.findIndex((i) => i.job === 'failing'); - if (foundFailingIndex !== -1) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`, - executions: instruction[foundFailingIndex]?.executions ?? 2 - }); - } - - return cy.task( - 'provideDataJobsExecutions', - { - relativePathToFixtures - }, - { timeout: DataPipelinesBasePO.WAIT_EXTRA_LONG_TASK } - ); + return cy.task( + "provideDataJobsExecutions", + { + relativePathToFixtures, + }, + { timeout: DataPipelinesBasePO.WAIT_EXTRA_LONG_TASK }, + ); + } + + /** + * ** Change long-lived Data Job status. + * + * @param {'failing'} instruction + * @param {boolean} status + * @returns {Cypress.Chainable} + */ + static changeLongLivedJobStatus(instruction, status) { + let pathToFixture; + if (instruction === "failing") { + pathToFixture = `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`; } - /** - * ** Provide one execution for short-lived Data Job with deployment. - * - * @param {'v0'|'v1'|'v2'} jobVersion - * @returns {Cypress.Chainable} - */ - static provideExecutionsForShortLivedTestJobWithDeploy(...jobVersion) { - const relativePathToFixtures = []; - if (jobVersion.includes('v0')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.json`, - executions: 1 - }); - } - - if (jobVersion.includes('v1')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.json`, - executions: 1 - }); - } - - if (jobVersion.includes('v2')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.json`, - executions: 1 - }); - } - - return cy.task( - 'provideDataJobsExecutions', - { - relativePathToFixtures - }, - { timeout: DataPipelinesBasePO.WAIT_EXTRA_LONG_TASK } - ); + return cy.task( + "changeJobsStatusesFixtures", + { + relativePathToFixtures: [ + { + pathToFixture, + }, + ], + status, + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ); + } + + /** + * ** Change short-lived Data Job with deployment status. + * + * @param {'v0'|'v1'|'v2'} jobVersion + * @param {boolean} status + * @returns {Cypress.Chainable} + */ + static changeShortLivedTestJobWithDeployStatus(jobVersion, status) { + let jobName; + + if (jobVersion === "v0") { + jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0; + } else if (jobVersion === "v1") { + jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1; + } else if (jobVersion === "v2") { + jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2; } - /** - * ** Change long-lived Data Job status. - * - * @param {'failing'} instruction - * @param {boolean} status - * @returns {Cypress.Chainable} - */ - static changeLongLivedJobStatus(instruction, status) { - let pathToFixture; - if (instruction === 'failing') { - pathToFixture = `/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`; - } - - return cy.task( - 'changeJobsStatusesFixtures', + return cy.task( + "changeJobsStatusesFixtures", + { + relativePathToFixtures: [ + { + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${jobName}.json`, + }, + ], + status, + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ); + } + + /** + * ** Create base short-lived test jobs without deployments. + * + * - They won't be deployed + * - They are created during tests execution and are deleted before and after test suits. + * - Executed in context of test environment. + * + * @returns {Cypress.Chainable} + */ + static createShortLivedTestJobsNoDeploy() { + return cy.task( + "createDeployJobs", + { + relativePathToFixtures: [ + { + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V10}.json`, + }, + { + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V11}.json`, + }, + ], + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ); + } + + /** + * ** Create additional short-lived test job without deployment. + * + * - It won't be deployed + * - It's created during tests execution and is deleted before and after test suits. + * - Executed in context of test environment. + * + * @returns {Cypress.Chainable} + */ + static createAdditionalShortLivedTestJobsNoDeploy() { + return cy + .task( + "createDeployJobs", + { + relativePathToFixtures: [ { - relativePathToFixtures: [ - { - pathToFixture - } - ], - status + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V12}.json`, }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ); + ], + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ) + .then(() => cy.wait(DataPipelinesBasePO.WAIT_AFTER_API_MODIFY_CALL)); + } + + /** + * ** Wait for Test Data Job existing execution to complete. + * + * @param {'v0'|'v1'|'v2'} jobVersion + */ + static waitForShortLivedTestJobWithDeployExecutionToComplete(jobVersion) { + return this.loadShortLivedTestJobFixtureWithDeploy(jobVersion).then( + (jobFixture) => + cy.task( + "waitForDataJobExecutionToComplete", + { + jobFixture, + jobExecutionTimeout: 240000, + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ), + ); + } + + /** + * ** Delete short-lived test jobs. + * + * - They are created during tests execution and are deleted before and after test suits. + * - Executed in context of test environment. + * + * @param {boolean} isDeleteOptional + * @returns {Cypress.Chainable} + */ + static deleteShortLivedTestJobsNoDeploy(isDeleteOptional) { + return cy.task( + "deleteJobsFixtures", + { + relativePathToFixtures: [ + { + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V10}.json`, + }, + { + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V11}.json`, + }, + { + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V12}.json`, + }, + ], + optional: isDeleteOptional, + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ); + } + + /** + * ** Delete short-lived test job with deployment. + * + * - They are created during tests execution and are deleted before and after test suits. + * - Executed in context of test environment. + * + * @param {'v0'|'v1'|'v2'} jobVersion + * @returns {Cypress.Chainable} + */ + static deleteShortLivedTestJobWithDeploy(...jobVersion) { + const relativePathToFixtures = []; + if (jobVersion.includes("v0")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.json`, + }); } - /** - * ** Change short-lived Data Job with deployment status. - * - * @param {'v0'|'v1'|'v2'} jobVersion - * @param {boolean} status - * @returns {Cypress.Chainable} - */ - static changeShortLivedTestJobWithDeployStatus(jobVersion, status) { - let jobName; - - if (jobVersion === 'v0') { - jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0; - } else if (jobVersion === 'v1') { - jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1; - } else if (jobVersion === 'v2') { - jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2; - } - - return cy.task( - 'changeJobsStatusesFixtures', - { - relativePathToFixtures: [ - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${jobName}.json` - } - ], - status - }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ); - } - - /** - * ** Create base short-lived test jobs without deployments. - * - * - They won't be deployed - * - They are created during tests execution and are deleted before and after test suits. - * - Executed in context of test environment. - * - * @returns {Cypress.Chainable} - */ - static createShortLivedTestJobsNoDeploy() { - return cy.task( - 'createDeployJobs', - { - relativePathToFixtures: [ - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V10}.json` - }, - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V11}.json` - } - ] - }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ); + if (jobVersion.includes("v1")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.json`, + }); } - /** - * ** Create additional short-lived test job without deployment. - * - * - It won't be deployed - * - It's created during tests execution and is deleted before and after test suits. - * - Executed in context of test environment. - * - * @returns {Cypress.Chainable} - */ - static createAdditionalShortLivedTestJobsNoDeploy() { - return cy - .task( - 'createDeployJobs', - { - relativePathToFixtures: [ - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V12}.json` - } - ] - }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ) - .then(() => cy.wait(DataPipelinesBasePO.WAIT_AFTER_API_MODIFY_CALL)); + if (jobVersion.includes("v2")) { + relativePathToFixtures.push({ + pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.json`, + }); } - /** - * ** Wait for Test Data Job existing execution to complete. - * - * @param {'v0'|'v1'|'v2'} jobVersion - */ - static waitForShortLivedTestJobWithDeployExecutionToComplete(jobVersion) { - return this.loadShortLivedTestJobFixtureWithDeploy(jobVersion).then((jobFixture) => - cy.task( - 'waitForDataJobExecutionToComplete', - { - jobFixture, - jobExecutionTimeout: 240000 - }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ) - ); + return cy.task( + "deleteJobsFixtures", + { + relativePathToFixtures, + }, + { timeout: DataPipelinesBasePO.WAIT_LONG_TASK }, + ); + } + + /** + * ** Load long-lived failing Data Job fixture. + * + * @returns {Cypress.Chainable<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} + */ + static loadLongLivedFailingJobFixture() { + return cy + .fixture(`/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`) + .then((fixture) => applyGlobalEnvSettings(fixture)); + } + + /** + * ** Load short-lived test Data Job with deployment fixture. + * + * @param {'v0'|'v1'|'v2'} jobVersion + * @returns {Cypress.Chainable<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} + */ + static loadShortLivedTestJobFixtureWithDeploy(jobVersion) { + let jobName; + + if (jobVersion === "v0") { + jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0; + } else if (jobVersion === "v1") { + jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1; + } else if (jobVersion === "v2") { + jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2; } - /** - * ** Delete short-lived test jobs. - * - * - They are created during tests execution and are deleted before and after test suits. - * - Executed in context of test environment. - * - * @param {boolean} isDeleteOptional - * @returns {Cypress.Chainable} - */ - static deleteShortLivedTestJobsNoDeploy(isDeleteOptional) { - return cy.task( - 'deleteJobsFixtures', - { - relativePathToFixtures: [ - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V10}.json` - }, - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V11}.json` - }, - { - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V12}.json` - } - ], - optional: isDeleteOptional - }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ); - } - - /** - * ** Delete short-lived test job with deployment. - * - * - They are created during tests execution and are deleted before and after test suits. - * - Executed in context of test environment. - * - * @param {'v0'|'v1'|'v2'} jobVersion - * @returns {Cypress.Chainable} - */ - static deleteShortLivedTestJobWithDeploy(...jobVersion) { - const relativePathToFixtures = []; - if (jobVersion.includes('v0')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0}.json` - }); - } - - if (jobVersion.includes('v1')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1}.json` - }); - } - - if (jobVersion.includes('v2')) { - relativePathToFixtures.push({ - pathToFixture: `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2}.json` - }); - } - - return cy.task( - 'deleteJobsFixtures', - { - relativePathToFixtures - }, - { timeout: DataPipelinesBasePO.WAIT_LONG_TASK } - ); - } - - /** - * ** Load long-lived failing Data Job fixture. - * - * @returns {Cypress.Chainable<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} - */ - static loadLongLivedFailingJobFixture() { - return cy.fixture(`/base/data-jobs/${TEAM_VDK}/${TEAM_VDK_DATA_JOB_FAILING}.json`).then((fixture) => applyGlobalEnvSettings(fixture)); - } - - /** - * ** Load short-lived test Data Job with deployment fixture. - * - * @param {'v0'|'v1'|'v2'} jobVersion - * @returns {Cypress.Chainable<{job_name:string; description:string; team:string; config:{db_default_type:string; contacts:{}; schedule:{schedule_cron:string}; generate_keytab:boolean; enable_execution_notifications:boolean}}>} - */ - static loadShortLivedTestJobFixtureWithDeploy(jobVersion) { - let jobName; - - if (jobVersion === 'v0') { - jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V0; - } else if (jobVersion === 'v1') { - jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V1; - } else if (jobVersion === 'v2') { - jobName = TEAM_VDK_DATA_JOB_TEST_WITH_DEPLOY_V2; - } - - return cy.fixture(`/base/data-jobs/${TEAM_VDK}/short-lived/${jobName}.json`).then((fixture) => applyGlobalEnvSettings(fixture)); - } - - /** - * ** Load short-lived test Data Jobs fixture. - * - * @returns {Cypress.Chainable>} - */ - static loadShortLivedTestJobsFixtureNoDeploy() { - return cy.fixture(`/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V10}.json`).then((fixture1) => { - return cy.fixture(`/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V11}.json`).then((fixture2) => { - return cy.fixture(`/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V12}.json`).then((fixture3) => { - return [applyGlobalEnvSettings(fixture1), applyGlobalEnvSettings(fixture2), applyGlobalEnvSettings(fixture3)]; - }); - }); - }); - } - - /** - * ** Wait for Data Job patch deployment req. - * - * @return {Cypress.Chainable} - */ - waitForDataJobDeploymentPatchReqInterceptor() { - return DataPipelinesBasePO.waitForDataJobDeploymentPatchReqInterceptor(); - } - - /** - * ** Wait for Data Job patch deployment req. - * - * @return {Cypress.Chainable} - */ - waitForDataJobExecutionPostReqInterceptor() { - return DataPipelinesBasePO.waitForDataJobExecutionPostReqInterceptor(); - } - - /** - * ** Wait API request to finish, then grid to load and finally additional short wait rows to be rendered. - * - * @return {Cypress.Chainable} - */ - waitForGridDataLoad() { - return this.waitForDataJobsApiGetReqInterceptor() - .then(() => this.waitForGridToLoad(null)) - .then(() => this.waitForViewToRenderShort()); - } - - /* Selectors */ - - getContentContainer() { - return cy.get('div.content-container'); + return cy + .fixture(`/base/data-jobs/${TEAM_VDK}/short-lived/${jobName}.json`) + .then((fixture) => applyGlobalEnvSettings(fixture)); + } + + /** + * ** Load short-lived test Data Jobs fixture. + * + * @returns {Cypress.Chainable>} + */ + static loadShortLivedTestJobsFixtureNoDeploy() { + return cy + .fixture( + `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V10}.json`, + ) + .then((fixture1) => { + return cy + .fixture( + `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V11}.json`, + ) + .then((fixture2) => { + return cy + .fixture( + `/base/data-jobs/${TEAM_VDK}/short-lived/${TEAM_VDK_DATA_JOB_TEST_V12}.json`, + ) + .then((fixture3) => { + return [ + applyGlobalEnvSettings(fixture1), + applyGlobalEnvSettings(fixture2), + applyGlobalEnvSettings(fixture3), + ]; + }); + }); + }); + } + + /** + * ** Wait for Data Job patch deployment req. + * + * @return {Cypress.Chainable} + */ + waitForDataJobDeploymentPatchReqInterceptor() { + return DataPipelinesBasePO.waitForDataJobDeploymentPatchReqInterceptor(); + } + + /** + * ** Wait for Data Job patch deployment req. + * + * @return {Cypress.Chainable} + */ + waitForDataJobExecutionPostReqInterceptor() { + return DataPipelinesBasePO.waitForDataJobExecutionPostReqInterceptor(); + } + + /** + * ** Wait API request to finish, then grid to load and finally additional short wait rows to be rendered. + * + * @return {Cypress.Chainable} + */ + waitForGridDataLoad() { + return this.waitForDataJobsApiGetReqInterceptor() + .then(() => this.waitForGridToLoad(null)) + .then(() => this.waitForViewToRenderShort()); + } + + /* Selectors */ + + getContentContainer() { + return cy.get("div.content-container"); + } + + /* Actions */ + + /** + ** Confirm action in dialog. + * + * @param {() => void} interceptor + */ + confirmInConfirmDialog(interceptor) { + cy.get("[data-cy=confirmation-dialog-ok-btn]") + .should("exist") + .click({ force: true }); + + if (interceptor) { + interceptor(); } - /* Actions */ + this.waitForViewToRenderShort(); + } - /** - ** Confirm action in dialog. - * - * @param {() => void} interceptor - */ - confirmInConfirmDialog(interceptor) { - cy.get('[data-cy=confirmation-dialog-ok-btn]').should('exist').click({ force: true }); + clickOnContentContainer() { + this.getContentContainer().should("exist").click({ force: true }); - if (interceptor) { - interceptor(); - } - - this.waitForViewToRenderShort(); - } - - clickOnContentContainer() { - this.getContentContainer().should('exist').click({ force: true }); - - this.waitForSmartDelay(); - } + this.waitForSmartDelay(); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/data-jobs.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/data-jobs.po.js index 6420b5f2ae..7cd8b32aef 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/data-jobs.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/data-jobs.po.js @@ -5,76 +5,82 @@ /// -import { DataJobsBasePO } from '../../base/data-pipelines/data-jobs-base.po'; +import { DataJobsBasePO } from "../../base/data-pipelines/data-jobs-base.po"; export class DataJobsExplorePage extends DataJobsBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobsExplorePage} - */ - static getPage() { - return new DataJobsExplorePage(); - } - - /** - * @inheritDoc - * @return {DataJobsExplorePage} - */ - static navigateWithSideMenu() { - return super.navigateWithSideMenu('navLinkExploreDataJobs', 'openExplore', { - before: () => { - this.waitForApplicationBootstrap(); - this.waitForDataJobsApiGetReqInterceptor(3); - }, - after: () => { - this.waitForDataJobsApiGetReqInterceptor(); + /** + * ** Returns instance of the page object. + * + * @returns {DataJobsExplorePage} + */ + static getPage() { + return new DataJobsExplorePage(); + } - const page = this.getPage(); - page.waitForGridToLoad(null); - page.waitForViewToRenderShort(); - } - }); - } + /** + * @inheritDoc + * @return {DataJobsExplorePage} + */ + static navigateWithSideMenu() { + return super.navigateWithSideMenu("navLinkExploreDataJobs", "openExplore", { + before: () => { + this.waitForApplicationBootstrap(); + this.waitForDataJobsApiGetReqInterceptor(3); + }, + after: () => { + this.waitForDataJobsApiGetReqInterceptor(); - /** - * ** Navigate to Explore Data Jobs. - * - * @return {DataJobsExplorePage} - */ - static navigateTo() { - /** - * @type {DataJobsExplorePage} - */ - const page = super.navigateTo('explore'); + const page = this.getPage(); page.waitForGridToLoad(null); page.waitForViewToRenderShort(); + }, + }); + } - return page; - } - + /** + * ** Navigate to Explore Data Jobs. + * + * @return {DataJobsExplorePage} + */ + static navigateTo() { /** - * ** Wait until Data grid is loaded. - * - * @param {string} contextSelector - * @param {number} timeout - * @returns {Cypress.Chainable} + * @type {DataJobsExplorePage} */ - waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { - return this._waitForGridToLoad('data-pipelines-explore-data-jobs', timeout); - } + const page = super.navigateTo("explore"); + page.waitForGridToLoad(null); + page.waitForViewToRenderShort(); + + return page; + } + + /** + * ** Wait until Data grid is loaded. + * + * @param {string} contextSelector + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { + return this._waitForGridToLoad("data-pipelines-explore-data-jobs", timeout); + } - // Selectors + // Selectors - getDataGrid() { - return cy.get('[data-cy=data-pipelines-explore-grid]'); - } + getDataGrid() { + return cy.get("[data-cy=data-pipelines-explore-grid]"); + } - getDataGridNavigateBtn(team, job) { - return cy.get('[data-cy=data-pipelines-explore-grid-details-link][data-job-params="' + team + ';' + job + '"]'); - } + getDataGridNavigateBtn(team, job) { + return cy.get( + '[data-cy=data-pipelines-explore-grid-details-link][data-job-params="' + + team + + ";" + + job + + '"]', + ); + } - getDataGridRefreshButton() { - return cy.get('[data-cy=data-pipelines-explore-refresh-btn]'); - } + getDataGridRefreshButton() { + return cy.get("[data-cy=data-pipelines-explore-refresh-btn]"); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/details/data-job-details.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/details/data-job-details.po.js index 473ec16b82..b6e5c70024 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/details/data-job-details.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/details/data-job-details.po.js @@ -5,25 +5,25 @@ /// -import { DataJobDetailsBasePO } from '../../../base/data-pipelines/data-job-details-base.po'; +import { DataJobDetailsBasePO } from "../../../base/data-pipelines/data-job-details-base.po"; export class DataJobExploreDetailsPage extends DataJobDetailsBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobExploreDetailsPage} - */ - static getPage() { - return new DataJobExploreDetailsPage(); - } + /** + * ** Returns instance of the page object. + * + * @returns {DataJobExploreDetailsPage} + */ + static getPage() { + return new DataJobExploreDetailsPage(); + } - /** - * @inheritDoc - * @param {string} teamName - * @param {string} jobName - * @return {DataJobExploreDetailsPage} - */ - static navigateTo(teamName, jobName) { - return super.navigateTo('explore', teamName, jobName); - } + /** + * @inheritDoc + * @param {string} teamName + * @param {string} jobName + * @return {DataJobExploreDetailsPage} + */ + static navigateTo(teamName, jobName) { + return super.navigateTo("explore", teamName, jobName); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/executions/data-job-executions.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/executions/data-job-executions.po.js index defb7c56f3..b2b1a20b01 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/executions/data-job-executions.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/explore/data-jobs/executions/data-job-executions.po.js @@ -5,25 +5,25 @@ /// -import { DataJobBasePO } from '../../../base/data-pipelines/data-job-base.po'; +import { DataJobBasePO } from "../../../base/data-pipelines/data-job-base.po"; export class DataJobExploreExecutionsPage extends DataJobBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobExploreExecutionsPage} - */ - static getPage() { - return new DataJobExploreExecutionsPage(); - } + /** + * ** Returns instance of the page object. + * + * @returns {DataJobExploreExecutionsPage} + */ + static getPage() { + return new DataJobExploreExecutionsPage(); + } - /** - * @inheritDoc - * @param {string} teamName - * @param {string} jobName - * @return {DataJobExploreExecutionsPage} - */ - static navigateTo(teamName, jobName) { - return super.navigateTo('explore', teamName, jobName, 'executions'); - } + /** + * @inheritDoc + * @param {string} teamName + * @param {string} jobName + * @return {DataJobExploreExecutionsPage} + */ + static navigateTo(teamName, jobName) { + return super.navigateTo("explore", teamName, jobName, "executions"); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page-data-jobs-health-overview.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page-data-jobs-health-overview.po.js index 35a4f7b7bd..c047ca9a69 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page-data-jobs-health-overview.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page-data-jobs-health-overview.po.js @@ -5,191 +5,209 @@ /// -import { DataPipelinesBasePO } from '../base/data-pipelines/data-pipelines-base.po'; +import { DataPipelinesBasePO } from "../base/data-pipelines/data-pipelines-base.po"; export class GetStartedDataJobsHealthOverviewWidgetPO extends DataPipelinesBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {GetStartedDataJobsHealthOverviewWidgetPO} - */ - static getPage() { - return new GetStartedDataJobsHealthOverviewWidgetPO(); - } - - /** - * ** Navigate to home page through URL and return instance of page object. - * - * @type {GetStartedDataJobsHealthOverviewWidgetPO} - */ - static navigateTo() { - cy.visit('/get-started'); - - this.waitForApplicationBootstrap(); - this.waitForDataJobsApiGetReqInterceptor(3); - - this.waitForViewToRenderShort(); - - return this.getPage(); - } - - /** - * ** Get Data jobs health panel. - * - * @returns {Cypress.Chainable>} - */ - getDataJobsHealthPanel() { - return cy.get('[data-cy=dp-data-jobs-health-panel]'); - } - - /** - * ** Get Execution status gauge Widget. - * - * @returns {Cypress.Chainable>} - */ - getExecutionStatusGaugeWidget() { - return this.getDataJobsHealthPanel().should('exist').find('lib-widget-execution-status-gauge'); - } - - /** - * ** Get percentage of successful executions against total from Execution status gauge Widget. - * - * @returns {Cypress.Chainable} - */ - getExecutionsSuccessPercentage() { - return this.getExecutionStatusGaugeWidget() - .should('exist') - .find('[data-cy=dp-jobs-executions-status-gauge-widget-percentage]') - .invoke('text') - .invoke('trim') - .invoke('replace', /%/, '') - .then((value) => +value); - } - - /** - * ** Get number of failed executions from Execution status gauge Widget. - * - * @returns {Cypress.Chainable} - */ - getNumberOfFailedExecutions() { - return this.getExecutionStatusGaugeWidget() - .should('exist') - .find('.value-current') - .invoke('text') - .invoke('trim') - .invoke('replace', /\s\w+/, '') - .then((value) => +value); - } - - /** - * ** Get number of total executions from Execution status gauge Widget. - * - * @returns {Cypress.Chainable} - */ - getExecutionsTotal() { - return this.getExecutionStatusGaugeWidget() - .should('exist') - .find('[data-cy=dp-jobs-executions-status-gauge-widget-total]') - .invoke('text') - .invoke('trim') - .invoke('replace', /\s\w+/, '') - .then((value) => +value); - } - - /** - * ** Get Failing Data jobs Widget. - * - * @returns {Cypress.Chainable>} - */ - getFailingJobsWidget() { - return this.getDataJobsHealthPanel().should('exist').find('lib-data-jobs-failed-widget'); - } - - /** - * ** Get all Data jobs from Failing Data jobs Widget. - * - * @returns {Cypress.Chainable>} - */ - getAllFailingJobs() { - return this.getFailingJobsWidget().should('exist').find('clr-dg-row clr-dg-cell.job-name-column'); - } - - /** - * ** Get all Data jobs links from Failing Data jobs Widget. - * - * @returns {Cypress.Chainable>} - */ - getAllFailingJobsLinks() { - return this.getFailingJobsWidget().should('exist').find('[data-cy=dp-failed-data-jobs-widget-job-name-link]'); - } - - /** - * ** Get Most Recent Failing Data jobs Widget. - * - * @returns {Cypress.Chainable>} - */ - getMostRecentFailingJobsWidget() { - return this.getDataJobsHealthPanel().should('exist').find('lib-data-jobs-executions-widget'); - } - - /** - * ** Get all Data jobs from Most Recent Failing Data jobs Widget. - * - * @returns {Cypress.Chainable>} - */ - getAllMostRecentFailingJobs() { - return this.getMostRecentFailingJobsWidget().should('exist').find('clr-dg-row clr-dg-cell.job-name-column'); - } - - /** - * ** Get all executions from Most Recent Failing Data jobs executions Widget. - * - * @returns {Cypress.Chainable>} - */ - getAllMostRecentFailingJobsLinks() { - return this.getMostRecentFailingJobsWidget().should('exist').find('[data-cy=dp-failed-data-jobs-executions-widget-job-name-link]'); - } - - // Actions - - navigateToFailingJobDetails(jobName) { - this._navigateToDataJob(this.getAllFailingJobsLinks(), jobName, 4); - } - - navigateToMostRecentFailingJobExecutions(jobName) { - this._navigateToDataJob(this.getAllMostRecentFailingJobsLinks(), jobName, 3); - } - - /** - * ** Navigate to Data job - * - * @param {Cypress.Chainable>} chainable - * @param {string} jobName - * @param {number} numberOfReqToWait - * @private - */ - _navigateToDataJob(chainable, jobName, numberOfReqToWait = 3) { - chainable - .then((elements) => { - /** - * @type {HTMLElement} - */ - let foundElement; - elements.each((_index, el) => { - if (el.innerText.trim().includes(jobName)) { - foundElement = el; - - return false; - } - }); - - return foundElement; - }) - .should('exist') - .click({ force: true }); - - this.waitForDataJobsApiGetReqInterceptor(numberOfReqToWait); - - this.waitForViewToRender(); - } + /** + * ** Returns instance of the page object. + * + * @returns {GetStartedDataJobsHealthOverviewWidgetPO} + */ + static getPage() { + return new GetStartedDataJobsHealthOverviewWidgetPO(); + } + + /** + * ** Navigate to home page through URL and return instance of page object. + * + * @type {GetStartedDataJobsHealthOverviewWidgetPO} + */ + static navigateTo() { + cy.visit("/get-started"); + + this.waitForApplicationBootstrap(); + this.waitForDataJobsApiGetReqInterceptor(3); + + this.waitForViewToRenderShort(); + + return this.getPage(); + } + + /** + * ** Get Data jobs health panel. + * + * @returns {Cypress.Chainable>} + */ + getDataJobsHealthPanel() { + return cy.get("[data-cy=dp-data-jobs-health-panel]"); + } + + /** + * ** Get Execution status gauge Widget. + * + * @returns {Cypress.Chainable>} + */ + getExecutionStatusGaugeWidget() { + return this.getDataJobsHealthPanel() + .should("exist") + .find("lib-widget-execution-status-gauge"); + } + + /** + * ** Get percentage of successful executions against total from Execution status gauge Widget. + * + * @returns {Cypress.Chainable} + */ + getExecutionsSuccessPercentage() { + return this.getExecutionStatusGaugeWidget() + .should("exist") + .find("[data-cy=dp-jobs-executions-status-gauge-widget-percentage]") + .invoke("text") + .invoke("trim") + .invoke("replace", /%/, "") + .then((value) => +value); + } + + /** + * ** Get number of failed executions from Execution status gauge Widget. + * + * @returns {Cypress.Chainable} + */ + getNumberOfFailedExecutions() { + return this.getExecutionStatusGaugeWidget() + .should("exist") + .find(".value-current") + .invoke("text") + .invoke("trim") + .invoke("replace", /\s\w+/, "") + .then((value) => +value); + } + + /** + * ** Get number of total executions from Execution status gauge Widget. + * + * @returns {Cypress.Chainable} + */ + getExecutionsTotal() { + return this.getExecutionStatusGaugeWidget() + .should("exist") + .find("[data-cy=dp-jobs-executions-status-gauge-widget-total]") + .invoke("text") + .invoke("trim") + .invoke("replace", /\s\w+/, "") + .then((value) => +value); + } + + /** + * ** Get Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getFailingJobsWidget() { + return this.getDataJobsHealthPanel() + .should("exist") + .find("lib-data-jobs-failed-widget"); + } + + /** + * ** Get all Data jobs from Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getAllFailingJobs() { + return this.getFailingJobsWidget() + .should("exist") + .find("clr-dg-row clr-dg-cell.job-name-column"); + } + + /** + * ** Get all Data jobs links from Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getAllFailingJobsLinks() { + return this.getFailingJobsWidget() + .should("exist") + .find("[data-cy=dp-failed-data-jobs-widget-job-name-link]"); + } + + /** + * ** Get Most Recent Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getMostRecentFailingJobsWidget() { + return this.getDataJobsHealthPanel() + .should("exist") + .find("lib-data-jobs-executions-widget"); + } + + /** + * ** Get all Data jobs from Most Recent Failing Data jobs Widget. + * + * @returns {Cypress.Chainable>} + */ + getAllMostRecentFailingJobs() { + return this.getMostRecentFailingJobsWidget() + .should("exist") + .find("clr-dg-row clr-dg-cell.job-name-column"); + } + + /** + * ** Get all executions from Most Recent Failing Data jobs executions Widget. + * + * @returns {Cypress.Chainable>} + */ + getAllMostRecentFailingJobsLinks() { + return this.getMostRecentFailingJobsWidget() + .should("exist") + .find("[data-cy=dp-failed-data-jobs-executions-widget-job-name-link]"); + } + + // Actions + + navigateToFailingJobDetails(jobName) { + this._navigateToDataJob(this.getAllFailingJobsLinks(), jobName, 4); + } + + navigateToMostRecentFailingJobExecutions(jobName) { + this._navigateToDataJob( + this.getAllMostRecentFailingJobsLinks(), + jobName, + 3, + ); + } + + /** + * ** Navigate to Data job + * + * @param {Cypress.Chainable>} chainable + * @param {string} jobName + * @param {number} numberOfReqToWait + * @private + */ + _navigateToDataJob(chainable, jobName, numberOfReqToWait = 3) { + chainable + .then((elements) => { + /** + * @type {HTMLElement} + */ + let foundElement; + elements.each((_index, el) => { + if (el.innerText.trim().includes(jobName)) { + foundElement = el; + + return false; + } + }); + + return foundElement; + }) + .should("exist") + .click({ force: true }); + + this.waitForDataJobsApiGetReqInterceptor(numberOfReqToWait); + + this.waitForViewToRender(); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page.po.js index 729a399ebd..75c38b9d6f 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/get-started/get-started-page.po.js @@ -5,42 +5,42 @@ /// -import { BasePagePO } from '../base/base-page.po'; +import { BasePagePO } from "../base/base-page.po"; export class GetStartedPagePO extends BasePagePO { - /** - * ** Returns instance of the page object. - * - * @returns {GetStartedPagePO} - */ - static getPage() { - return new GetStartedPagePO(); - } - - /** - * ** Navigate to home page through URL and return instance of page object. - * - * @type {GetStartedPagePO} - */ - static navigateTo() { - cy.visit('/get-started'); - - this.waitForApplicationBootstrap(); - this.waitForDataJobsApiGetReqInterceptor(3); - - this.waitForViewToRenderShort(); - - return this.getPage(); - } - - /** - * ** Navigate to home page through URL and return instance of page object. - * ** Do not wait for bootstrap and interceptors - * @type {GetStartedPagePO} - */ - static navigateToNoBootstrap() { - cy.visit('/get-started'); - this.waitForViewToRenderShort(); - return this.getPage(); - } + /** + * ** Returns instance of the page object. + * + * @returns {GetStartedPagePO} + */ + static getPage() { + return new GetStartedPagePO(); + } + + /** + * ** Navigate to home page through URL and return instance of page object. + * + * @type {GetStartedPagePO} + */ + static navigateTo() { + cy.visit("/get-started"); + + this.waitForApplicationBootstrap(); + this.waitForDataJobsApiGetReqInterceptor(3); + + this.waitForViewToRenderShort(); + + return this.getPage(); + } + + /** + * ** Navigate to home page through URL and return instance of page object. + * ** Do not wait for bootstrap and interceptors + * @type {GetStartedPagePO} + */ + static navigateToNoBootstrap() { + cy.visit("/get-started"); + this.waitForViewToRenderShort(); + return this.getPage(); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/data-jobs.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/data-jobs.po.js index ebbb755c83..af93aa68a5 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/data-jobs.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/data-jobs.po.js @@ -5,159 +5,170 @@ /// -import { DataJobsBasePO } from '../../base/data-pipelines/data-jobs-base.po'; +import { DataJobsBasePO } from "../../base/data-pipelines/data-jobs-base.po"; export class DataJobsManagePage extends DataJobsBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobsManagePage} - */ - static getPage() { - return new DataJobsManagePage(); - } - - /** - * ** Navigate to page with provided nav link id through side menu navigation, choose Team, and return instance of page object. - * - * @return {DataJobsManagePage} - */ - static navigateWithSideMenu() { - return super.navigateWithSideMenu('navLinkManageDataJobs', 'openManage', { - before: () => { - this.waitForApplicationBootstrap(); - this.waitForDataJobsApiGetReqInterceptor(3); - }, - after: () => { - this.waitForDataJobsApiGetReqInterceptor(); - - const page = this.getPage(); - page.waitForGridToLoad(null); - page.waitForViewToRenderShort(); - } - }); - } - - /** - * ** Navigate to Manage Data Jobs. - * - * @return {DataJobsManagePage} - */ - static navigateTo() { - /** - * @type {DataJobsManagePage} - */ - const page = super.navigateTo('manage'); + /** + * ** Returns instance of the page object. + * + * @returns {DataJobsManagePage} + */ + static getPage() { + return new DataJobsManagePage(); + } + + /** + * ** Navigate to page with provided nav link id through side menu navigation, choose Team, and return instance of page object. + * + * @return {DataJobsManagePage} + */ + static navigateWithSideMenu() { + return super.navigateWithSideMenu("navLinkManageDataJobs", "openManage", { + before: () => { + this.waitForApplicationBootstrap(); + this.waitForDataJobsApiGetReqInterceptor(3); + }, + after: () => { + this.waitForDataJobsApiGetReqInterceptor(); + + const page = this.getPage(); page.waitForGridToLoad(null); page.waitForViewToRenderShort(); - - return page; - } - + }, + }); + } + + /** + * ** Navigate to Manage Data Jobs. + * + * @return {DataJobsManagePage} + */ + static navigateTo() { /** - * ** Navigate to home page through URL and return instance of page object. - * ** Do not wait for bootstrap and interceptors - * @type {GetStartedPagePO} + * @type {DataJobsManagePage} */ - static navigateToNoBootstrap() { - /** - * @type {DataJobsManagePage} - */ - const page = super.navigateToNoBootstrap('manage'); - page.waitForGridToLoad(null); - return page; - } - + const page = super.navigateTo("manage"); + page.waitForGridToLoad(null); + page.waitForViewToRenderShort(); + + return page; + } + + /** + * ** Navigate to home page through URL and return instance of page object. + * ** Do not wait for bootstrap and interceptors + * @type {GetStartedPagePO} + */ + static navigateToNoBootstrap() { /** - * ** Wait until Data grid is loaded. - * - * @param {string} contextSelector - * @param {number} timeout - * @returns {Cypress.Chainable} + * @type {DataJobsManagePage} */ - waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { - return this._waitForGridToLoad('data-pipelines-manage-data-jobs', timeout); - } - - getDataGrid() { - return cy.get('[data-cy=data-pipelines-manage-grid]'); - } - - getDataGridNavigateBtn(team, job) { - return cy.get('[data-cy=data-pipelines-manage-grid-details-link][data-job-params="' + team + ';' + job + '"]'); - } - - getDataGridRefreshButton() { - return cy.get('[data-cy=data-pipelines-manage-refresh-btn]'); - } - - getExecuteNowGridButton() { - return cy.get('[data-cy=data-pipelines-manage-grid-execute-btn]'); - } - - getJobStatus(jobName) { - return this.getDataGridRowByName(jobName).then(($row) => { - if ($row.find('[data-cy=data-pipelines-job-disabled]').length) { - return 'disable'; - } - - if ($row.find('[data-cy=data-pipelines-job-enabled]').length) { - return 'enable'; - } - - return 'not_deployed'; - }); - } - - // Actions - - executeDataJob(jobName) { - this.selectRow(jobName); - - this.waitForClickThinkingTime(); - - this.getExecuteNowGridButton().should('exist').click({ force: true }); - - this.waitForViewToRenderShort(); - - this.confirmInConfirmDialog(() => { - this.waitForDataJobExecutionPostReqInterceptor(); - }); - } - - changeStatus(newStatus) { - cy.get(`[data-cy=data-pipelines-job-${newStatus}-btn]`).should('exist').should('be.enabled').click({ force: true }); - } - - toggleJobStatus(jobName) { - this.selectRow(jobName); - - this.getJobStatus(jobName).then((currentStatus) => { - if (currentStatus === 'not_deployed') { - throw new Error('Data job is not Deployed.'); - } - - const newStatus = currentStatus === 'enable' ? 'disable' : 'enable'; - - cy.log(`Current status: ${currentStatus}, new status: ${newStatus}`); - - this.changeStatus(newStatus); - - this.confirmInConfirmDialog(() => { - this.waitForDataJobDeploymentPatchReqInterceptor(); - }); - - this.getToastTitle().should('exist').should('contain.text', 'Status update completed'); - - this.getToastDismiss().should('exist').click({ force: true }); - - this.waitForClickThinkingTime(); // Natural wait for User action + const page = super.navigateToNoBootstrap("manage"); + page.waitForGridToLoad(null); + return page; + } - this.refreshDataGrid(); + /** + * ** Wait until Data grid is loaded. + * + * @param {string} contextSelector + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { + return this._waitForGridToLoad("data-pipelines-manage-data-jobs", timeout); + } + + getDataGrid() { + return cy.get("[data-cy=data-pipelines-manage-grid]"); + } + + getDataGridNavigateBtn(team, job) { + return cy.get( + '[data-cy=data-pipelines-manage-grid-details-link][data-job-params="' + + team + + ";" + + job + + '"]', + ); + } + + getDataGridRefreshButton() { + return cy.get("[data-cy=data-pipelines-manage-refresh-btn]"); + } + + getExecuteNowGridButton() { + return cy.get("[data-cy=data-pipelines-manage-grid-execute-btn]"); + } + + getJobStatus(jobName) { + return this.getDataGridRowByName(jobName).then(($row) => { + if ($row.find("[data-cy=data-pipelines-job-disabled]").length) { + return "disable"; + } + + if ($row.find("[data-cy=data-pipelines-job-enabled]").length) { + return "enable"; + } + + return "not_deployed"; + }); + } + + // Actions + + executeDataJob(jobName) { + this.selectRow(jobName); + + this.waitForClickThinkingTime(); + + this.getExecuteNowGridButton().should("exist").click({ force: true }); + + this.waitForViewToRenderShort(); + + this.confirmInConfirmDialog(() => { + this.waitForDataJobExecutionPostReqInterceptor(); + }); + } + + changeStatus(newStatus) { + cy.get(`[data-cy=data-pipelines-job-${newStatus}-btn]`) + .should("exist") + .should("be.enabled") + .click({ force: true }); + } + + toggleJobStatus(jobName) { + this.selectRow(jobName); + + this.getJobStatus(jobName).then((currentStatus) => { + if (currentStatus === "not_deployed") { + throw new Error("Data job is not Deployed."); + } + + const newStatus = currentStatus === "enable" ? "disable" : "enable"; + + cy.log(`Current status: ${currentStatus}, new status: ${newStatus}`); + + this.changeStatus(newStatus); + + this.confirmInConfirmDialog(() => { + this.waitForDataJobDeploymentPatchReqInterceptor(); + }); + + this.getToastTitle() + .should("exist") + .should("contain.text", "Status update completed"); + + this.getToastDismiss().should("exist").click({ force: true }); + + this.waitForClickThinkingTime(); // Natural wait for User action + + this.refreshDataGrid(); - this.getJobStatus(jobName).then((changedStatus) => { - expect(changedStatus).to.equal(newStatus); - }); - }); - } + this.getJobStatus(jobName).then((changedStatus) => { + expect(changedStatus).to.equal(newStatus); + }); + }); + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/details/data-job-details.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/details/data-job-details.po.js index 0ed68a2fc9..8fb42e0ae0 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/details/data-job-details.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/details/data-job-details.po.js @@ -5,257 +5,294 @@ /// -import { DataJobDetailsBasePO } from '../../../base/data-pipelines/data-job-details-base.po'; +import { DataJobDetailsBasePO } from "../../../base/data-pipelines/data-job-details-base.po"; export class DataJobManageDetailsPage extends DataJobDetailsBasePO { - /** - * ** Returns instance of the page object. - * - * @returns {DataJobManageDetailsPage} - */ - static getPage() { - return new DataJobManageDetailsPage(); + /** + * ** Returns instance of the page object. + * + * @returns {DataJobManageDetailsPage} + */ + static getPage() { + return new DataJobManageDetailsPage(); + } + + /** + * @inheritDoc + * @return {DataJobManageDetailsPage} + */ + static navigateTo(teamName, jobName) { + return super.navigateTo("manage", teamName, jobName); + } + + // Deployment methods + + // Acceptable values are "not-deployed", "enabled", "disabled" + getDeploymentStatus(status) { + return cy.get("[data-cy=data-pipelines-job-details-status-" + status + "]"); + } + + // Description methods + + getDescription() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-description] .form-section-readonly", + ); + } + + getDescriptionEditButton() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-description] .form-section-header > .btn", + ); + } + + getDescriptionEditTextarea() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-description] textarea", + ); + } + + getDescriptionSaveButton() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-description] button:contains(Save)", + ); + } + + openDescription() { + this.getDescriptionEditButton().click({ force: true }); + } + + enterDescriptionDetails(description) { + this.getDescriptionEditTextarea().clear().type(description); + } + + saveDescription() { + this.getDescriptionSaveButton().click({ force: true }); + + this.waitForDataJobPutReqInterceptor(); + } + + /** + * ** Format ISO DateTime to Data job executions timeline format. + * + * e.g. + * '2023-03-27T10:25:24.960717Z' -> 'Mar 27, 2023, 10:25 AM UTC' + * + * @param {string} isoDate + */ + formatDateTimeFromISOToExecutionsTimeline(isoDate) { + const dateTimeChunks = isoDate.replace(/(\..*)?Z$/, "").split("T"); + const dateChunk = dateTimeChunks[0]; + const timeChunk = dateTimeChunks[1]; + + return `${this._formatDateFromISOToExecutionsTimeline(dateChunk)}, ${this._formatTimeFromISOToExecutionsTimeline(timeChunk)} UTC`; + } + + // Schedule methods + + getSchedule() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-schedule] .form-section-readonly", + ); + } + + // Disable/Enable methods + + getStatusEditButton() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-status] .form-section-header > .btn", + ); + } + + getStatusSaveButton() { + return cy.get( + "[data-cy=data-pipelines-data-job-details-status] button:contains(Save)", + ); + } + + // Executions Timeline + + getExecutionsSteps() { + return cy.get(".clr-timeline-step"); + } + + getExecutionStepStartedTile(stepSelector) { + return cy + .get(stepSelector) + .should("exist") + .scrollIntoView() + .find("[data-cy=data-pipelines-executions-timeline-started]") + .invoke("attr", "title"); + } + + getExecutionStepEndedTile(stepSelector) { + return cy + .get(stepSelector) + .should("exist") + .find("[data-cy=data-pipelines-executions-timeline-ended]") + .invoke("attr", "title"); + } + + getExecutionStepManualTriggerer(stepSelector) { + return cy + .get(stepSelector) + .find("[data-cy=data-pipelines-executions-timeline-manual-start]"); + } + + getExecutionStepStatusIcon(stepSelector, status) { + const STATUS_ICON_MAP = {}; + STATUS_ICON_MAP["SUBMITTED"] = "hourglass"; + STATUS_ICON_MAP["RUNNING"] = "play"; + STATUS_ICON_MAP["SUCCEEDED"] = "success-standard"; + STATUS_ICON_MAP["CANCELLED"] = "times-circle"; + STATUS_ICON_MAP["SKIPPED"] = "circle-arrow"; + STATUS_ICON_MAP["USER_ERROR"] = "error-standard"; + STATUS_ICON_MAP["PLATFORM_ERROR"] = "error-standard"; + + if (status === "RUNNING") { + return cy.get(stepSelector).find(`clr-spinner[aria-label='In progress']`); } - /** - * @inheritDoc - * @return {DataJobManageDetailsPage} - */ - static navigateTo(teamName, jobName) { - return super.navigateTo('manage', teamName, jobName); - } - - // Deployment methods - - // Acceptable values are "not-deployed", "enabled", "disabled" - getDeploymentStatus(status) { - return cy.get('[data-cy=data-pipelines-job-details-status-' + status + ']'); - } - - // Description methods - - getDescription() { - return cy.get('[data-cy=data-pipelines-data-job-details-description] .form-section-readonly'); - } - - getDescriptionEditButton() { - return cy.get('[data-cy=data-pipelines-data-job-details-description] .form-section-header > .btn'); - } - - getDescriptionEditTextarea() { - return cy.get('[data-cy=data-pipelines-data-job-details-description] textarea'); - } - - getDescriptionSaveButton() { - return cy.get('[data-cy=data-pipelines-data-job-details-description] button:contains(Save)'); - } - - openDescription() { - this.getDescriptionEditButton().click({ force: true }); - } - - enterDescriptionDetails(description) { - this.getDescriptionEditTextarea().clear().type(description); - } - - saveDescription() { - this.getDescriptionSaveButton().click({ force: true }); - - this.waitForDataJobPutReqInterceptor(); - } + return cy.get(stepSelector).find(`[shape=${STATUS_ICON_MAP[status]}]`); + } - /** - * ** Format ISO DateTime to Data job executions timeline format. - * - * e.g. - * '2023-03-27T10:25:24.960717Z' -> 'Mar 27, 2023, 10:25 AM UTC' - * - * @param {string} isoDate - */ - formatDateTimeFromISOToExecutionsTimeline(isoDate) { - const dateTimeChunks = isoDate.replace(/(\..*)?Z$/, '').split('T'); - const dateChunk = dateTimeChunks[0]; - const timeChunk = dateTimeChunks[1]; - - return `${this._formatDateFromISOToExecutionsTimeline(dateChunk)}, ${this._formatTimeFromISOToExecutionsTimeline(timeChunk)} UTC`; - } + // Actions - // Schedule methods + changeStatus(currentStatus) { + const newStatus = + currentStatus.trim().toLowerCase() === "enabled" ? "disable" : "enable"; - getSchedule() { - return cy.get('[data-cy=data-pipelines-data-job-details-schedule] .form-section-readonly'); - } + return cy + .get(`[data-cy=data-pipelines-data-job-details-status-${newStatus}]`) + .should("exist") + .check({ force: true }); + } - // Disable/Enable methods + toggleJobStatus() { + cy.get("[data-cy=data-pipelines-job-details-status]") + .invoke("text") + .then((jobStatus) => { + this.getStatusEditButton().scrollIntoView().click({ force: true }); - getStatusEditButton() { - return cy.get('[data-cy=data-pipelines-data-job-details-status] .form-section-header > .btn'); - } - - getStatusSaveButton() { - return cy.get('[data-cy=data-pipelines-data-job-details-status] button:contains(Save)'); - } + this.changeStatus(jobStatus); - // Executions Timeline + this.waitForClickThinkingTime(); - getExecutionsSteps() { - return cy.get('.clr-timeline-step'); - } + this.getStatusSaveButton().scrollIntoView().click({ force: true }); - getExecutionStepStartedTile(stepSelector) { - return cy.get(stepSelector).should('exist').scrollIntoView().find('[data-cy=data-pipelines-executions-timeline-started]').invoke('attr', 'title'); - } + let newStatus = jobStatus === "Enabled" ? "Disabled" : "Enabled"; - getExecutionStepEndedTile(stepSelector) { - return cy.get(stepSelector).should('exist').find('[data-cy=data-pipelines-executions-timeline-ended]').invoke('attr', 'title'); - } + this.waitForDataJobDeploymentPatchReqInterceptor(); - getExecutionStepManualTriggerer(stepSelector) { - return cy.get(stepSelector).find('[data-cy=data-pipelines-executions-timeline-manual-start]'); - } + this.getToastTitle() + .should("exist") + .should("contain.text", "Status update completed"); - getExecutionStepStatusIcon(stepSelector, status) { - const STATUS_ICON_MAP = {}; - STATUS_ICON_MAP['SUBMITTED'] = 'hourglass'; - STATUS_ICON_MAP['RUNNING'] = 'play'; - STATUS_ICON_MAP['SUCCEEDED'] = 'success-standard'; - STATUS_ICON_MAP['CANCELLED'] = 'times-circle'; - STATUS_ICON_MAP['SKIPPED'] = 'circle-arrow'; - STATUS_ICON_MAP['USER_ERROR'] = 'error-standard'; - STATUS_ICON_MAP['PLATFORM_ERROR'] = 'error-standard'; - - if (status === 'RUNNING') { - return cy.get(stepSelector).find(`clr-spinner[aria-label='In progress']`); - } - - return cy.get(stepSelector).find(`[shape=${STATUS_ICON_MAP[status]}]`); - } + this.waitForActionThinkingTime(); // Natural wait for User action - // Actions + this.getToastDismiss().should("exist").click({ force: true }); - changeStatus(currentStatus) { - const newStatus = currentStatus.trim().toLowerCase() === 'enabled' ? 'disable' : 'enable'; + cy.get("[data-cy=data-pipelines-job-details-status]") + .scrollIntoView() + .should("have.text", newStatus); + }); + } - return cy.get(`[data-cy=data-pipelines-data-job-details-status-${newStatus}]`).should('exist').check({ force: true }); + /** + * ** Format Date chunk to Data job Executions timeline format. + * + * e.g. + * '2023-03-27' -> 'Mar 27, 2023' + * + * @param {string} dateChunk + * @return {string} + * @private + */ + _formatDateFromISOToExecutionsTimeline(dateChunk) { + if (!dateChunk) { + return ""; } - toggleJobStatus() { - cy.get('[data-cy=data-pipelines-job-details-status]') - .invoke('text') - .then((jobStatus) => { - this.getStatusEditButton().scrollIntoView().click({ force: true }); - - this.changeStatus(jobStatus); - - this.waitForClickThinkingTime(); - - this.getStatusSaveButton().scrollIntoView().click({ force: true }); - - let newStatus = jobStatus === 'Enabled' ? 'Disabled' : 'Enabled'; - - this.waitForDataJobDeploymentPatchReqInterceptor(); - - this.getToastTitle().should('exist').should('contain.text', 'Status update completed'); - - this.waitForActionThinkingTime(); // Natural wait for User action - - this.getToastDismiss().should('exist').click({ force: true }); - - cy.get('[data-cy=data-pipelines-job-details-status]').scrollIntoView().should('have.text', newStatus); - }); + const dateChunks = dateChunk.split("-"); + const year = dateChunks[0]; + const month = dateChunks[1]; + const day = parseInt(dateChunks[2], 10); + + const dayMonth = `${day}, ${year}`; + + let monthAbbreviation; + + switch (month) { + case "01": + monthAbbreviation = "Jan"; + break; + case "02": + monthAbbreviation = "Feb"; + break; + case "03": + monthAbbreviation = "Mar"; + break; + case "04": + monthAbbreviation = "Apr"; + break; + case "05": + monthAbbreviation = "May"; + break; + case "06": + monthAbbreviation = "Jun"; + break; + case "07": + monthAbbreviation = "Jul"; + break; + case "08": + monthAbbreviation = "Aug"; + break; + case "09": + monthAbbreviation = "Sep"; + break; + case "10": + monthAbbreviation = "Oct"; + break; + case "11": + monthAbbreviation = "Nov"; + break; + case "12": + monthAbbreviation = "Dec"; + break; + default: + monthAbbreviation = ""; } - /** - * ** Format Date chunk to Data job Executions timeline format. - * - * e.g. - * '2023-03-27' -> 'Mar 27, 2023' - * - * @param {string} dateChunk - * @return {string} - * @private - */ - _formatDateFromISOToExecutionsTimeline(dateChunk) { - if (!dateChunk) { - return ''; - } - - const dateChunks = dateChunk.split('-'); - const year = dateChunks[0]; - const month = dateChunks[1]; - const day = parseInt(dateChunks[2], 10); - - const dayMonth = `${day}, ${year}`; - - let monthAbbreviation; - - switch (month) { - case '01': - monthAbbreviation = 'Jan'; - break; - case '02': - monthAbbreviation = 'Feb'; - break; - case '03': - monthAbbreviation = 'Mar'; - break; - case '04': - monthAbbreviation = 'Apr'; - break; - case '05': - monthAbbreviation = 'May'; - break; - case '06': - monthAbbreviation = 'Jun'; - break; - case '07': - monthAbbreviation = 'Jul'; - break; - case '08': - monthAbbreviation = 'Aug'; - break; - case '09': - monthAbbreviation = 'Sep'; - break; - case '10': - monthAbbreviation = 'Oct'; - break; - case '11': - monthAbbreviation = 'Nov'; - break; - case '12': - monthAbbreviation = 'Dec'; - break; - default: - monthAbbreviation = ''; - } - - return `${monthAbbreviation} ${dayMonth}`; + return `${monthAbbreviation} ${dayMonth}`; + } + + /** + * ** Format Time chunk to Data job Executions timeline format. + * + * e.g. + * '10:25:24' -> '10:25 AM' + * + * @param {string} timeChunk + * @return {string} + * @private + */ + _formatTimeFromISOToExecutionsTimeline(timeChunk) { + if (!timeChunk) { + return ""; } - /** - * ** Format Time chunk to Data job Executions timeline format. - * - * e.g. - * '10:25:24' -> '10:25 AM' - * - * @param {string} timeChunk - * @return {string} - * @private - */ - _formatTimeFromISOToExecutionsTimeline(timeChunk) { - if (!timeChunk) { - return ''; - } - - const timeChunks = timeChunk.split(':'); - const hour = parseInt(timeChunks[0], 10); - const minute = parseInt(timeChunks[1], 10); - const beforeOrAfter = hour >= 12 ? 'PM' : 'AM'; - const hourNormalizedTo12Hours = hour > 12 ? hour % 12 : hour === 0 ? 12 : hour; - const hourNormalizedToString = hourNormalizedTo12Hours < 10 ? `0${hourNormalizedTo12Hours}` : `${hourNormalizedTo12Hours}`; - const minuteNormalizedToString = minute < 10 ? `0${minute}` : `${minute}`; - - return `${hourNormalizedToString}:${minuteNormalizedToString} ${beforeOrAfter}`; - } + const timeChunks = timeChunk.split(":"); + const hour = parseInt(timeChunks[0], 10); + const minute = parseInt(timeChunks[1], 10); + const beforeOrAfter = hour >= 12 ? "PM" : "AM"; + const hourNormalizedTo12Hours = + hour > 12 ? hour % 12 : hour === 0 ? 12 : hour; + const hourNormalizedToString = + hourNormalizedTo12Hours < 10 + ? `0${hourNormalizedTo12Hours}` + : `${hourNormalizedTo12Hours}`; + const minuteNormalizedToString = minute < 10 ? `0${minute}` : `${minute}`; + + return `${hourNormalizedToString}:${minuteNormalizedToString} ${beforeOrAfter}`; + } } diff --git a/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/executions/data-job-executions.po.js b/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/executions/data-job-executions.po.js index fdabfcc458..2f3d031760 100644 --- a/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/executions/data-job-executions.po.js +++ b/projects/frontend/data-pipelines/gui/e2e/support/pages/manage/data-jobs/executions/data-job-executions.po.js @@ -5,658 +5,778 @@ /// -import { DataJobBasePO } from '../../../base/data-pipelines/data-job-base.po'; -import { DataJobsBasePO } from '../../../base/data-pipelines/data-jobs-base.po'; +import { DataJobBasePO } from "../../../base/data-pipelines/data-job-base.po"; +import { DataJobsBasePO } from "../../../base/data-pipelines/data-jobs-base.po"; export class DataJobManageExecutionsPage extends DataJobBasePO { + /** + * ** Returns instance of the page object. + * + * @returns {DataJobManageExecutionsPage} + */ + static getPage() { + return new DataJobManageExecutionsPage(); + } + + /** + * @inheritDoc + * @return {DataJobManageExecutionsPage} + */ + static navigateTo(teamName, jobName) { /** - * ** Returns instance of the page object. - * - * @returns {DataJobManageExecutionsPage} + * @type {DataJobManageExecutionsPage} */ - static getPage() { - return new DataJobManageExecutionsPage(); - } - - /** - * @inheritDoc - * @return {DataJobManageExecutionsPage} - */ - static navigateTo(teamName, jobName) { + const page = super.navigateTo("manage", teamName, jobName, "executions"); + page.waitForGridToLoad(null); + page.waitForViewToRenderShort(); + + return page; + } + + /** + * ** Navigate to Data Job executions page with provided URL. + * + * @param {string} url + * @return {DataJobManageExecutionsPage} + */ + static navigateToExecutionsWithUrl(url) { + const page = this.navigateToDataJobUrl(url, 3); + + page.waitForGridToLoad(null); + page.waitForViewToRenderShort(); + + return page; + } + + /* Utils */ + + /** + * ** Converts from view time to seconds. + * + * @example + * + * - val=1m 34s + * - val=45s + * + * @return {number} + */ + convertStringContentToSeconds(val) { + const params = val.trim().split(" "); + + if (params.length === 2) { + return ( + parseInt(params[0].trim().replace(/m$/, ""), 10) * 60 + + parseInt(params[1].trim().replace(/s$/, ""), 10) + ); + } else { + return parseInt(params[0].trim().replace(/s$/, ""), 10); + } + } + + /* Selectors */ + + /** + * ** Wait until Data grid is loaded. + * + * @param {string} contextSelector + * @param {number} timeout + * @returns {Cypress.Chainable} + */ + waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { + return this._waitForGridToLoad( + "data-pipelines-data-job-executions", + timeout, + ); + } + + // General + + getExecRefreshBtn() { + return cy.get("[data-cy=data-pipelines-job-executions-refresh-btn"); + } + + getExecLoadingSpinner() { + return cy.get("[data-cy=data-pipelines-job-executions-loading-spinner]"); + } + + // Charts + + getStatusChart() { + return cy.get("[data-cy=data-pipelines-job-executions-status-chart]"); + } + + getDurationChart() { + return cy.get("[data-cy=data-pipelines-job-executions-duration-chart]"); + } + + getTimePeriod() { + return cy.get("[data-cy=data-pipelines-job-executions-time-period]"); + } + + // DataGrid + + getDataGrid() { + return cy.get("[data-cy=data-pipelines-job-executions-datagrid]", { + timeout: DataJobManageExecutionsPage.WAIT_SHORT_TASK, + }); + } + + getDataGridPopupFilter() { + return cy.get(".datagrid-filter.clr-popover-content"); + } + + getDataGridInputFilter() { + return this.getDataGridPopupFilter().should("exist").find("input"); + } + + getDataGridPopupFilterCloseBtn() { + return this.getDataGridPopupFilter().should("exist").find(".close"); + } + + getDataGridSpinner() { + return this.getDataGrid().should("exist").find(".datagrid-spinner"); + } + + // Header + + // status + getDataGridExecStatusHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-status-header]"); + } + + getDataGridExecStatusFilterOpenerBtn() { + return this.getDataGridExecStatusHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecStatusFilters() { + return this.getDataGridPopupFilter() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-status-filters]"); + } + + getDataGridExecStatusSortBtn() { + return this.getDataGridExecStatusHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + /** + * ** Get status filter label + * + * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status + * @return {Cypress.Chainable>} + */ + getDataGridExecStatusFilterLabel(status) { + return cy.get(`[data-cy=dp-job-executions-status-filter-label-${status}]`); + } + + /** + * ** Get status filter checkbox + * + * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status + * @return {Cypress.Chainable>} + */ + getDataGridExecStatusFilterCheckbox(status) { + return cy.get( + `[data-cy=dp-job-executions-status-filter-checkbox-${status}]`, + ); + } + + getDataGridExecStatusFilterCheckboxesStatuses() { + return cy + .get("[data-cy=dp-job-executions-status-filter-checkbox] input") + .then(($checkboxes) => { + return cy.wrap( + Array.from($checkboxes).map((checkbox) => { + const key = checkbox.getAttribute("data-cy").split("-").pop(); + const value = checkbox.checked; + + return [key, value]; + }), + ); + }); + } + + // type + getDataGridExecTypeHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-type-header]"); + } + + getDataGridExecTypeFilterOpenerBtn() { + return this.getDataGridExecTypeHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecTypeSortBtn() { + return this.getDataGridExecTypeHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + /** + * ** Get type filter label + * + * @param {'manual'|'scheduled'} type + * @return {Cypress.Chainable>} + */ + getDataGridExecTypeFilterLabel(type) { + return cy.get(`[data-cy=dp-job-executions-type-filter-label-${type}]`); + } + + /** + * ** Get type filter checkbox + * + * @param {'manual'|'scheduled'} type + * @return {Cypress.Chainable>} + */ + getDataGridExecTypeFilterCheckbox(type) { + return cy.get(`[data-cy=dp-job-executions-type-filter-checkbox-${type}]`); + } + + getDataGridExecTypeFilterCheckboxesStatuses() { + return cy + .get("[data-cy=dp-job-executions-type-filter-checkbox] input") + .then(($checkboxes) => { + return cy.wrap( + Array.from($checkboxes).map((checkbox) => { + const key = checkbox.getAttribute("data-cy").split("-").pop(); + const value = checkbox.checked; + + return [key, value]; + }), + ); + }); + } + + // duration + getDataGridExecDurationHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-duration-header]"); + } + + getDataGridExecDurationFilterOpenerBtn() { + return this.getDataGridExecDurationHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecDurationSortBtn() { + return this.getDataGridExecDurationHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + // exec start + getDataGridExecStartHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-start-header]"); + } + + getDataGridExecStartFilterOpenerBtn() { + return this.getDataGridExecStartHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecStartSortBtn() { + return this.getDataGridExecStartHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + // exec end + getDataGridExecEndHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-end-header]"); + } + + getDataGridExecEndFilterOpenerBtn() { + return this.getDataGridExecEndHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecEndSortBtn() { + return this.getDataGridExecEndHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + // id + getDataGridExecIDHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-id-header]"); + } + + getDataGridExecIDFilterOpenerBtn() { + return this.getDataGridExecIDHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecIDSortBtn() { + return this.getDataGridExecIDHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + // version + getDataGridExecVersionHeader() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-version-header]"); + } + + getDataGridExecVersionFilterOpenerBtn() { + return this.getDataGridExecVersionHeader() + .should("exist") + .find(".datagrid-filter-toggle"); + } + + getDataGridExecVersionSortBtn() { + return this.getDataGridExecVersionHeader() + .should("exist") + .find(".datagrid-column-title"); + } + + // Rows and Cells + + getDataGridRows() { + return this.getDataGrid().should("exist").find("clr-dg-row.datagrid-row"); + } + + getDataGridRow(rowIndex) { + return this.getDataGridRows() + .should("have.length.gte", rowIndex - 1) + .then((rows) => Array.from(rows)[rowIndex - 1]); + } + + getDataGridCells(rowIndex) { + return this.getDataGridRow(rowIndex) + .should("exist") + .find("clr-dg-cell.datagrid-cell"); + } + + getDataGridCellByIndex(rowIndex, cellIndex) { + if (rowIndex) { + return this.getDataGridCells(rowIndex) + .should("have.length.gte", cellIndex - 1) + .then((cells) => Array.from(cells)[cellIndex - 1]); + } + + return this.getDataGridRows() + .should("have.length.gte", 0) + .then(($rows) => { + return $rows.reduce((accumulator, row) => { + const _cells = Array.from( + row.querySelectorAll("clr-dg-cell.datagrid-cell"), + ); + + if (_cells[cellIndex - 1]) { + accumulator.push(_cells[cellIndex - 1]); + } + + return accumulator; + }, []); + }); + } + + getDataGridCellByIdentifier(rowIndex, identifier) { + return this.getDataGridRow(rowIndex).should("exist").find(identifier); + } + + getDataGridCellsByIdentifier(identifier) { + return this.getDataGrid().should("exist").find(identifier); + } + + // type + getDataGridExecTypeCell(rowIndex) { + return this.getDataGridCellByIdentifier( + rowIndex, + "[data-cy=data-pipelines-job-executions-type-cell]", + ); + } + + /** + * ** Get containers in cells for given type. + * + * @param {'manual'|'scheduled'} type + * @return {Cypress.Chainable>} + */ + getDataGridExecTypeContainers(type) { + return this.getDataGrid() + .should("exist") + .then(($gridContainer) => { /** - * @type {DataJobManageExecutionsPage} + * @type {JQuery} */ - const page = super.navigateTo('manage', teamName, jobName, 'executions'); - page.waitForGridToLoad(null); - page.waitForViewToRenderShort(); - - return page; - } - - /** - * ** Navigate to Data Job executions page with provided URL. - * - * @param {string} url - * @return {DataJobManageExecutionsPage} - */ - static navigateToExecutionsWithUrl(url) { - const page = this.navigateToDataJobUrl(url, 3); - - page.waitForGridToLoad(null); - page.waitForViewToRenderShort(); - - return page; - } - - /* Utils */ - - /** - * ** Converts from view time to seconds. - * - * @example - * - * - val=1m 34s - * - val=45s - * - * @return {number} - */ - convertStringContentToSeconds(val) { - const params = val.trim().split(' '); - - if (params.length === 2) { - return parseInt(params[0].trim().replace(/m$/, ''), 10) * 60 + parseInt(params[1].trim().replace(/s$/, ''), 10); - } else { - return parseInt(params[0].trim().replace(/s$/, ''), 10); + const $containers = $gridContainer.find( + "[data-cy=data-pipelines-job-executions-type-container]", + ); + + return cy.wrap($containers.length === 0 ? [] : $containers); + }) + .then(($containers) => { + return cy.wrap( + Array.from($containers) + .map((container) => container.getAttribute("title").toLowerCase()) + .filter((title) => new RegExp(`^${type}`).test(title)), + ); + }); + } + + // getDataGridExecStatusFilterCheckboxesStatuses() { + // return cy.get('[data-cy=dp-job-executions-status-filter-checkbox] input') + // .then(($checkboxes) => { + // return cy.wrap(Array.from($checkboxes).map((checkbox) => { + // const key = checkbox.getAttribute('data-cy').split('-').pop(); + // const value = checkbox.checked; + // + // return [key, value]; + // })); + // }); + // } + + // duration + getDataGridExecDurationCell(rowIndex) { + return this.getDataGridCellByIdentifier( + rowIndex, + "[data-cy=data-pipelines-job-executions-duration-cell]", + ); + } + + // exec start + getDataGridExecStartCell(rowIndex) { + return this.getDataGridCellByIdentifier( + rowIndex, + "[data-cy=data-pipelines-job-executions-start-cell]", + ); + } + + getDataGridExecStartCells() { + return this.getDataGridCellsByIdentifier( + "[data-cy=data-pipelines-job-executions-start-cell]", + ); + } + + generateExecStartFilterValue() { + return this.getDataGridExecStartCells().then(($cells) => { + const pmCells = Array.from($cells).map((cell) => + /PM$/.test(`${cell.innerText?.trim()}`), + ); + if (pmCells.length >= 2) { + return cy.wrap("PM"); + } + + const amCells = Array.from($cells).map((cell) => + /AM$/.test(`${cell.innerText?.trim()}`), + ); + if (amCells.length >= 2) { + return cy.wrap("AM"); + } + + throw new Error("Unhandled use case in test scenarios"); + }); + } + + // exec end + getDataGridExecEndCell(rowIndex) { + return this.getDataGridCellByIdentifier( + rowIndex, + "[data-cy=data-pipelines-job-executions-end-cell]", + ); + } + + getDataGridExecEndCells() { + return this.getDataGridCellsByIdentifier( + "[data-cy=data-pipelines-job-executions-end-cell]", + ); + } + + generateExecEndFilterValue() { + return this.getDataGridExecEndCells().then(($cells) => { + const pmCells = Array.from($cells).map((cell) => + /PM$/.test(`${cell.innerText?.trim()}`), + ); + + if (pmCells.length > 0) { + return cy.wrap("PM"); + } + + return cy.wrap("AM"); + }); + } + + // id + getDataGridExecIDCell(rowIndex) { + return this.getDataGridCellByIdentifier( + rowIndex, + "[data-cy=data-pipelines-job-executions-id-cell]", + ); + } + + getDataGridExecIDCells() { + return this.getDataGridCellsByIdentifier( + "[data-cy=data-pipelines-job-executions-id-cell]", + ); + } + + // exec version + getDataGridExecVersionCell(rowIndex) { + return this.getDataGridCellByIdentifier( + rowIndex, + "[data-cy=data-pipelines-job-executions-job-version-cell]", + ); + } + + getDataGridExecVersionCells() { + return this.getDataGridCellsByIdentifier( + "[data-cy=data-pipelines-job-executions-job-version-cell]", + ); + } + + // Pagination + + getDataGridPagination() { + return this.getDataGrid() + .should("exist") + .find("[data-cy=data-pipelines-job-executions-datagrid-pagination]"); + } + + /* Actions */ + + // General + + refreshExecData() { + this.getExecRefreshBtn().should("exist").click({ force: true }); + } + + // DataGrid + + typeToTextFilterInput(value) { + this.getDataGridInputFilter().should("exist").type(value); + + this.waitForViewToRender(); + } + + clearTextFilterInput() { + this.getDataGridInputFilter().should("exist").clear({ force: true }); + + this.waitForViewToRender(); + } + + closeFilter() { + this.getDataGridPopupFilterCloseBtn() + .should("exist") + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + // Header + + // status + /** + * ** Open status filter. + */ + openStatusFilter() { + this.getDataGridExecStatusFilterOpenerBtn() + .should("exist") + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + /** + * ** Choose filter by some status. + * + * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status + */ + filterByStatus(status) { + this.getDataGridExecStatusFilterCheckbox(status) + .should("exist") + .as("checkbox") + .invoke("is", ":checked") + .then((checked) => { + if (!checked) { + this.getDataGridExecStatusFilterLabel(status) + .should("exist") + .click({ force: true }); } - } - - /* Selectors */ - - /** - * ** Wait until Data grid is loaded. - * - * @param {string} contextSelector - * @param {number} timeout - * @returns {Cypress.Chainable} - */ - waitForGridToLoad(contextSelector, timeout = DataJobsBasePO.WAIT_SHORT_TASK) { - return this._waitForGridToLoad('data-pipelines-data-job-executions', timeout); - } - - // General - - getExecRefreshBtn() { - return cy.get('[data-cy=data-pipelines-job-executions-refresh-btn'); - } - - getExecLoadingSpinner() { - return cy.get('[data-cy=data-pipelines-job-executions-loading-spinner]'); - } - - // Charts - - getStatusChart() { - return cy.get('[data-cy=data-pipelines-job-executions-status-chart]'); - } - - getDurationChart() { - return cy.get('[data-cy=data-pipelines-job-executions-duration-chart]'); - } - - getTimePeriod() { - return cy.get('[data-cy=data-pipelines-job-executions-time-period]'); - } - - // DataGrid - - getDataGrid() { - return cy.get('[data-cy=data-pipelines-job-executions-datagrid]', { - timeout: DataJobManageExecutionsPage.WAIT_SHORT_TASK - }); - } - - getDataGridPopupFilter() { - return cy.get('.datagrid-filter.clr-popover-content'); - } - - getDataGridInputFilter() { - return this.getDataGridPopupFilter().should('exist').find('input'); - } - - getDataGridPopupFilterCloseBtn() { - return this.getDataGridPopupFilter().should('exist').find('.close'); - } - - getDataGridSpinner() { - return this.getDataGrid().should('exist').find('.datagrid-spinner'); - } - - // Header - - // status - getDataGridExecStatusHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-status-header]'); - } - - getDataGridExecStatusFilterOpenerBtn() { - return this.getDataGridExecStatusHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecStatusFilters() { - return this.getDataGridPopupFilter().should('exist').find('[data-cy=data-pipelines-job-executions-status-filters]'); - } - - getDataGridExecStatusSortBtn() { - return this.getDataGridExecStatusHeader().should('exist').find('.datagrid-column-title'); - } - - /** - * ** Get status filter label - * - * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status - * @return {Cypress.Chainable>} - */ - getDataGridExecStatusFilterLabel(status) { - return cy.get(`[data-cy=dp-job-executions-status-filter-label-${status}]`); - } - - /** - * ** Get status filter checkbox - * - * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status - * @return {Cypress.Chainable>} - */ - getDataGridExecStatusFilterCheckbox(status) { - return cy.get(`[data-cy=dp-job-executions-status-filter-checkbox-${status}]`); - } - - getDataGridExecStatusFilterCheckboxesStatuses() { - return cy.get('[data-cy=dp-job-executions-status-filter-checkbox] input').then(($checkboxes) => { - return cy.wrap( - Array.from($checkboxes).map((checkbox) => { - const key = checkbox.getAttribute('data-cy').split('-').pop(); - const value = checkbox.checked; - - return [key, value]; - }) - ); - }); - } - - // type - getDataGridExecTypeHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-type-header]'); - } - - getDataGridExecTypeFilterOpenerBtn() { - return this.getDataGridExecTypeHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecTypeSortBtn() { - return this.getDataGridExecTypeHeader().should('exist').find('.datagrid-column-title'); - } - - /** - * ** Get type filter label - * - * @param {'manual'|'scheduled'} type - * @return {Cypress.Chainable>} - */ - getDataGridExecTypeFilterLabel(type) { - return cy.get(`[data-cy=dp-job-executions-type-filter-label-${type}]`); - } - - /** - * ** Get type filter checkbox - * - * @param {'manual'|'scheduled'} type - * @return {Cypress.Chainable>} - */ - getDataGridExecTypeFilterCheckbox(type) { - return cy.get(`[data-cy=dp-job-executions-type-filter-checkbox-${type}]`); - } - - getDataGridExecTypeFilterCheckboxesStatuses() { - return cy.get('[data-cy=dp-job-executions-type-filter-checkbox] input').then(($checkboxes) => { - return cy.wrap( - Array.from($checkboxes).map((checkbox) => { - const key = checkbox.getAttribute('data-cy').split('-').pop(); - const value = checkbox.checked; - - return [key, value]; - }) - ); - }); - } - - // duration - getDataGridExecDurationHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-duration-header]'); - } - - getDataGridExecDurationFilterOpenerBtn() { - return this.getDataGridExecDurationHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecDurationSortBtn() { - return this.getDataGridExecDurationHeader().should('exist').find('.datagrid-column-title'); - } - - // exec start - getDataGridExecStartHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-start-header]'); - } - - getDataGridExecStartFilterOpenerBtn() { - return this.getDataGridExecStartHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecStartSortBtn() { - return this.getDataGridExecStartHeader().should('exist').find('.datagrid-column-title'); - } - - // exec end - getDataGridExecEndHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-end-header]'); - } - - getDataGridExecEndFilterOpenerBtn() { - return this.getDataGridExecEndHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecEndSortBtn() { - return this.getDataGridExecEndHeader().should('exist').find('.datagrid-column-title'); - } - - // id - getDataGridExecIDHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-id-header]'); - } - - getDataGridExecIDFilterOpenerBtn() { - return this.getDataGridExecIDHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecIDSortBtn() { - return this.getDataGridExecIDHeader().should('exist').find('.datagrid-column-title'); - } - - // version - getDataGridExecVersionHeader() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-version-header]'); - } - - getDataGridExecVersionFilterOpenerBtn() { - return this.getDataGridExecVersionHeader().should('exist').find('.datagrid-filter-toggle'); - } - - getDataGridExecVersionSortBtn() { - return this.getDataGridExecVersionHeader().should('exist').find('.datagrid-column-title'); - } - - // Rows and Cells - - getDataGridRows() { - return this.getDataGrid().should('exist').find('clr-dg-row.datagrid-row'); - } - - getDataGridRow(rowIndex) { - return this.getDataGridRows() - .should('have.length.gte', rowIndex - 1) - .then((rows) => Array.from(rows)[rowIndex - 1]); - } - - getDataGridCells(rowIndex) { - return this.getDataGridRow(rowIndex).should('exist').find('clr-dg-cell.datagrid-cell'); - } - - getDataGridCellByIndex(rowIndex, cellIndex) { - if (rowIndex) { - return this.getDataGridCells(rowIndex) - .should('have.length.gte', cellIndex - 1) - .then((cells) => Array.from(cells)[cellIndex - 1]); + }); + + this.waitForViewToRender(); + } + + /** + * ** Clear filter by some status. + * + * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status + */ + clearFilterByStatus(status) { + this.getDataGridExecStatusFilterCheckbox(status) + .should("exist") + .as("checkbox") + .invoke("is", ":checked") + .then((checked) => { + if (checked) { + this.getDataGridExecStatusFilterLabel(status) + .should("exist") + .click({ force: true }); } + }); + + this.waitForViewToRender(); + } + + sortByExecStatus() { + this.getDataGridExecStatusSortBtn().should("exist").click({ force: true }); + + this.waitForViewToRender(); + } + + // type + openTypeFilter() { + this.getDataGridExecTypeFilterOpenerBtn() + .should("exist") + .click({ force: true }); + + this.waitForViewToRenderShort(); + } + + /** + * ** Choose filter by some type. + * + * @param {'manual'|'scheduled'} type + */ + filterByType(type) { + this.getDataGridExecTypeFilterCheckbox(type) + .should("exist") + .as("checkbox") + .invoke("is", ":checked") + .then((checked) => { + if (!checked) { + this.getDataGridExecTypeFilterLabel(type).click({ + force: true, + }); + } + }); + + this.waitForViewToRender(); + } + + /** + * ** Clear filter by some type. + * + * @param {'manual'|'scheduled'} type + */ + clearFilterByType(type) { + this.getDataGridExecTypeFilterCheckbox(type) + .should("exist") + .as("checkbox") + .invoke("is", ":checked") + .then((checked) => { + if (checked) { + this.getDataGridExecTypeFilterLabel(type) + .should("exist") + .click({ force: true }); + } + }); - return this.getDataGridRows() - .should('have.length.gte', 0) - .then(($rows) => { - return $rows.reduce((accumulator, row) => { - const _cells = Array.from(row.querySelectorAll('clr-dg-cell.datagrid-cell')); - - if (_cells[cellIndex - 1]) { - accumulator.push(_cells[cellIndex - 1]); - } - - return accumulator; - }, []); - }); - } - - getDataGridCellByIdentifier(rowIndex, identifier) { - return this.getDataGridRow(rowIndex).should('exist').find(identifier); - } - - getDataGridCellsByIdentifier(identifier) { - return this.getDataGrid().should('exist').find(identifier); - } - - // type - getDataGridExecTypeCell(rowIndex) { - return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-type-cell]'); - } - - /** - * ** Get containers in cells for given type. - * - * @param {'manual'|'scheduled'} type - * @return {Cypress.Chainable>} - */ - getDataGridExecTypeContainers(type) { - return this.getDataGrid() - .should('exist') - .then(($gridContainer) => { - /** - * @type {JQuery} - */ - const $containers = $gridContainer.find('[data-cy=data-pipelines-job-executions-type-container]'); - - return cy.wrap($containers.length === 0 ? [] : $containers); - }) - .then(($containers) => { - return cy.wrap( - Array.from($containers) - .map((container) => container.getAttribute('title').toLowerCase()) - .filter((title) => new RegExp(`^${type}`).test(title)) - ); - }); - } - - // getDataGridExecStatusFilterCheckboxesStatuses() { - // return cy.get('[data-cy=dp-job-executions-status-filter-checkbox] input') - // .then(($checkboxes) => { - // return cy.wrap(Array.from($checkboxes).map((checkbox) => { - // const key = checkbox.getAttribute('data-cy').split('-').pop(); - // const value = checkbox.checked; - // - // return [key, value]; - // })); - // }); - // } - - // duration - getDataGridExecDurationCell(rowIndex) { - return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-duration-cell]'); - } - - // exec start - getDataGridExecStartCell(rowIndex) { - return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-start-cell]'); - } - - getDataGridExecStartCells() { - return this.getDataGridCellsByIdentifier('[data-cy=data-pipelines-job-executions-start-cell]'); - } - - generateExecStartFilterValue() { - return this.getDataGridExecStartCells().then(($cells) => { - const pmCells = Array.from($cells).map((cell) => /PM$/.test(`${cell.innerText?.trim()}`)); - if (pmCells.length >= 2) { - return cy.wrap('PM'); - } - - const amCells = Array.from($cells).map((cell) => /AM$/.test(`${cell.innerText?.trim()}`)); - if (amCells.length >= 2) { - return cy.wrap('AM'); - } - - throw new Error('Unhandled use case in test scenarios'); - }); - } - - // exec end - getDataGridExecEndCell(rowIndex) { - return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-end-cell]'); - } - - getDataGridExecEndCells() { - return this.getDataGridCellsByIdentifier('[data-cy=data-pipelines-job-executions-end-cell]'); - } - - generateExecEndFilterValue() { - return this.getDataGridExecEndCells().then(($cells) => { - const pmCells = Array.from($cells).map((cell) => /PM$/.test(`${cell.innerText?.trim()}`)); - - if (pmCells.length > 0) { - return cy.wrap('PM'); - } - - return cy.wrap('AM'); - }); - } - - // id - getDataGridExecIDCell(rowIndex) { - return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-id-cell]'); - } - - getDataGridExecIDCells() { - return this.getDataGridCellsByIdentifier('[data-cy=data-pipelines-job-executions-id-cell]'); - } - - // exec version - getDataGridExecVersionCell(rowIndex) { - return this.getDataGridCellByIdentifier(rowIndex, '[data-cy=data-pipelines-job-executions-job-version-cell]'); - } - - getDataGridExecVersionCells() { - return this.getDataGridCellsByIdentifier('[data-cy=data-pipelines-job-executions-job-version-cell]'); - } - - // Pagination - - getDataGridPagination() { - return this.getDataGrid().should('exist').find('[data-cy=data-pipelines-job-executions-datagrid-pagination]'); - } - - /* Actions */ - - // General - - refreshExecData() { - this.getExecRefreshBtn().should('exist').click({ force: true }); - } - - // DataGrid - - typeToTextFilterInput(value) { - this.getDataGridInputFilter().should('exist').type(value); - - this.waitForViewToRender(); - } - - clearTextFilterInput() { - this.getDataGridInputFilter().should('exist').clear({ force: true }); - - this.waitForViewToRender(); - } - - closeFilter() { - this.getDataGridPopupFilterCloseBtn().should('exist').click({ force: true }); - - this.waitForViewToRenderShort(); - } - - // Header - - // status - /** - * ** Open status filter. - */ - openStatusFilter() { - this.getDataGridExecStatusFilterOpenerBtn().should('exist').click({ force: true }); - - this.waitForViewToRenderShort(); - } - - /** - * ** Choose filter by some status. - * - * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status - */ - filterByStatus(status) { - this.getDataGridExecStatusFilterCheckbox(status) - .should('exist') - .as('checkbox') - .invoke('is', ':checked') - .then((checked) => { - if (!checked) { - this.getDataGridExecStatusFilterLabel(status).should('exist').click({ force: true }); - } - }); - - this.waitForViewToRender(); - } - - /** - * ** Clear filter by some status. - * - * @param {'succeeded'|'platform_error'|'user_error'|'running'|'submitted'|'skipped'|'cancelled'} status - */ - clearFilterByStatus(status) { - this.getDataGridExecStatusFilterCheckbox(status) - .should('exist') - .as('checkbox') - .invoke('is', ':checked') - .then((checked) => { - if (checked) { - this.getDataGridExecStatusFilterLabel(status).should('exist').click({ force: true }); - } - }); - - this.waitForViewToRender(); - } - - sortByExecStatus() { - this.getDataGridExecStatusSortBtn().should('exist').click({ force: true }); + this.waitForViewToRender(); + } - this.waitForViewToRender(); - } + sortByExecType() { + this.getDataGridExecTypeSortBtn().should("exist").click({ force: true }); - // type - openTypeFilter() { - this.getDataGridExecTypeFilterOpenerBtn().should('exist').click({ force: true }); + this.waitForViewToRender(); + } - this.waitForViewToRenderShort(); - } + // duration + openDurationFilter() { + this.getDataGridExecDurationFilterOpenerBtn() + .should("exist") + .click({ force: true }); - /** - * ** Choose filter by some type. - * - * @param {'manual'|'scheduled'} type - */ - filterByType(type) { - this.getDataGridExecTypeFilterCheckbox(type) - .should('exist') - .as('checkbox') - .invoke('is', ':checked') - .then((checked) => { - if (!checked) { - this.getDataGridExecTypeFilterLabel(type).click({ - force: true - }); - } - }); - - this.waitForViewToRender(); - } + this.waitForViewToRenderShort(); + } - /** - * ** Clear filter by some type. - * - * @param {'manual'|'scheduled'} type - */ - clearFilterByType(type) { - this.getDataGridExecTypeFilterCheckbox(type) - .should('exist') - .as('checkbox') - .invoke('is', ':checked') - .then((checked) => { - if (checked) { - this.getDataGridExecTypeFilterLabel(type).should('exist').click({ force: true }); - } - }); - - this.waitForViewToRender(); - } + sortByExecDuration() { + this.getDataGridExecDurationSortBtn() + .should("exist") + .click({ force: true }); - sortByExecType() { - this.getDataGridExecTypeSortBtn().should('exist').click({ force: true }); + this.waitForViewToRender(); + } - this.waitForViewToRender(); - } + // exec start + openExecStartFilter() { + this.getDataGridExecStartFilterOpenerBtn() + .should("exist") + .click({ force: true }); - // duration - openDurationFilter() { - this.getDataGridExecDurationFilterOpenerBtn().should('exist').click({ force: true }); + this.waitForViewToRenderShort(); + } - this.waitForViewToRenderShort(); - } + sortByExecStart() { + this.getDataGridExecStartSortBtn().should("exist").click({ force: true }); - sortByExecDuration() { - this.getDataGridExecDurationSortBtn().should('exist').click({ force: true }); + this.waitForViewToRender(); + } - this.waitForViewToRender(); - } + // exec end + openExecEndFilter() { + this.getDataGridExecEndFilterOpenerBtn() + .should("exist") + .click({ force: true }); - // exec start - openExecStartFilter() { - this.getDataGridExecStartFilterOpenerBtn().should('exist').click({ force: true }); + this.waitForViewToRenderShort(); + } - this.waitForViewToRenderShort(); - } + sortByExecEnd() { + this.getDataGridExecEndSortBtn().should("exist").click({ force: true }); - sortByExecStart() { - this.getDataGridExecStartSortBtn().should('exist').click({ force: true }); + this.waitForViewToRender(); + } - this.waitForViewToRender(); - } + // id + openIDFilter() { + this.getDataGridExecIDFilterOpenerBtn() + .should("exist") + .click({ force: true }); - // exec end - openExecEndFilter() { - this.getDataGridExecEndFilterOpenerBtn().should('exist').click({ force: true }); + this.waitForViewToRenderShort(); + } - this.waitForViewToRenderShort(); - } + sortByExecID() { + this.getDataGridExecIDSortBtn().should("exist").click({ force: true }); - sortByExecEnd() { - this.getDataGridExecEndSortBtn().should('exist').click({ force: true }); + this.waitForViewToRender(); + } - this.waitForViewToRender(); - } + // version + openVersionFilter() { + this.getDataGridExecVersionFilterOpenerBtn() + .should("exist") + .click({ force: true }); - // id - openIDFilter() { - this.getDataGridExecIDFilterOpenerBtn().should('exist').click({ force: true }); + this.waitForViewToRenderShort(); + } - this.waitForViewToRenderShort(); - } - - sortByExecID() { - this.getDataGridExecIDSortBtn().should('exist').click({ force: true }); + sortByExecVersion() { + this.getDataGridExecVersionSortBtn().should("exist").click({ force: true }); - this.waitForViewToRender(); - } - - // version - openVersionFilter() { - this.getDataGridExecVersionFilterOpenerBtn().should('exist').click({ force: true }); - - this.waitForViewToRenderShort(); - } - - sortByExecVersion() { - this.getDataGridExecVersionSortBtn().should('exist').click({ force: true }); - - this.waitForViewToRender(); - } + this.waitForViewToRender(); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/karma.conf.js b/projects/frontend/data-pipelines/gui/projects/data-pipelines/karma.conf.js index 08e60c0f94..79730abe8a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/karma.conf.js +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/karma.conf.js @@ -7,55 +7,68 @@ // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-junit-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma')], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser + config.set({ + basePath: "", + frameworks: ["jasmine", "@angular-devkit/build-angular"], + plugins: [ + require("karma-jasmine"), + require("karma-chrome-launcher"), + require("karma-jasmine-html-reporter"), + require("karma-junit-reporter"), + require("karma-coverage"), + require("@angular-devkit/build-angular/plugins/karma"), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: require("path").join( + __dirname, + "../../reports/coverage/data-pipelines-lib", + ), + subdir: ".", + reporters: [ + //Code coverage - output in HTML file and Console(to be parsed in the CI/CD badge) + { type: "html" }, + { type: "text-summary" }, + { type: "lcovonly" }, + ], + check: { + global: { + lines: 80, }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../reports/coverage/data-pipelines-lib'), - subdir: '.', - reporters: [ - //Code coverage - output in HTML file and Console(to be parsed in the CI/CD badge) - { type: 'html' }, - { type: 'text-summary' }, - { type: 'lcovonly' } - ], - check: { - global: { - lines: 80 - } - } - }, - reporters: ['progress', 'junit', 'coverage'], - junitReporter: { - outputDir: require('path').join(__dirname, '../../reports/test-results/data-pipelines-lib'), - outputFile: 'unit-tests.xml', - useBrowserName: false - }, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['ChromeHeadless'], - customLaunchers: { - ChromeHeadless_No_Sandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - }, - singleRun: false, - restartOnFileChange: true - }); + }, + }, + reporters: ["progress", "junit", "coverage"], + junitReporter: { + outputDir: require("path").join( + __dirname, + "../../reports/test-results/data-pipelines-lib", + ), + outputFile: "unit-tests.xml", + useBrowserName: false, + }, + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ["ChromeHeadless"], + customLaunchers: { + ChromeHeadless_No_Sandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + singleRun: false, + restartOnFileChange: true, + }); }; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts index 6992df9056..92b1929911 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts @@ -3,1836 +3,2221 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fakeAsync, tick } from '@angular/core/testing'; - -import { CallFake, URLStateManager } from '@versatiledatakit/shared'; - -import { FILTER_KEY, FiltersSortManager, KeyValueTuple, SORT_KEY } from './filters-sort-manager'; - -const FILTER_CRITERIA_F1 = 'c1'; -const FILTER_CRITERIA_F2 = 'c2'; -const FILTER_CRITERIA_F3 = 'c3'; -const FILTER_CRITERIA_F4 = 'c4'; -const FILTER_CRITERIA_F5 = 'c5'; +import { fakeAsync, tick } from "@angular/core/testing"; + +import { CallFake, URLStateManager } from "@versatiledatakit/shared"; + +import { + FILTER_KEY, + FiltersSortManager, + KeyValueTuple, + SORT_KEY, +} from "./filters-sort-manager"; + +const FILTER_CRITERIA_F1 = "c1"; +const FILTER_CRITERIA_F2 = "c2"; +const FILTER_CRITERIA_F3 = "c3"; +const FILTER_CRITERIA_F4 = "c4"; +const FILTER_CRITERIA_F5 = "c5"; type FILTER_CRITERIA_UNION = - | typeof FILTER_CRITERIA_F1 - | typeof FILTER_CRITERIA_F2 - | typeof FILTER_CRITERIA_F3 - | typeof FILTER_CRITERIA_F4 - | typeof FILTER_CRITERIA_F5; - -const SORT_CRITERIA_S1 = 'c1'; -const SORT_CRITERIA_S2 = 'c2'; -const SORT_CRITERIA_S3 = 'c3'; -const SORT_CRITERIA_S4 = 'c4'; -type SORT_CRITERIA_UNION = typeof SORT_CRITERIA_S1 | typeof SORT_CRITERIA_S2 | typeof SORT_CRITERIA_S3 | typeof SORT_CRITERIA_S4; + | typeof FILTER_CRITERIA_F1 + | typeof FILTER_CRITERIA_F2 + | typeof FILTER_CRITERIA_F3 + | typeof FILTER_CRITERIA_F4 + | typeof FILTER_CRITERIA_F5; + +const SORT_CRITERIA_S1 = "c1"; +const SORT_CRITERIA_S2 = "c2"; +const SORT_CRITERIA_S3 = "c3"; +const SORT_CRITERIA_S4 = "c4"; +type SORT_CRITERIA_UNION = + | typeof SORT_CRITERIA_S1 + | typeof SORT_CRITERIA_S2 + | typeof SORT_CRITERIA_S3 + | typeof SORT_CRITERIA_S4; const KNOWN_FILTER_CRITERIA: FILTER_CRITERIA_UNION[] = [ - FILTER_CRITERIA_F1, - FILTER_CRITERIA_F2, - FILTER_CRITERIA_F3, - FILTER_CRITERIA_F4, - FILTER_CRITERIA_F5 + FILTER_CRITERIA_F1, + FILTER_CRITERIA_F2, + FILTER_CRITERIA_F3, + FILTER_CRITERIA_F4, + FILTER_CRITERIA_F5, ]; -const KNOWN_SORT_CRITERIA: SORT_CRITERIA_UNION[] = [SORT_CRITERIA_S1, SORT_CRITERIA_S2, SORT_CRITERIA_S3, SORT_CRITERIA_S4]; +const KNOWN_SORT_CRITERIA: SORT_CRITERIA_UNION[] = [ + SORT_CRITERIA_S1, + SORT_CRITERIA_S2, + SORT_CRITERIA_S3, + SORT_CRITERIA_S4, +]; class MutationObserver { - observer(_changes: Array>): void { - // No-op. - } + observer( + _changes: Array< + KeyValueTuple + >, + ): void { + // No-op. + } } -describe('FiltersSortManager', () => { - let urlStateManagerStub: jasmine.SpyObj; +describe("FiltersSortManager", () => { + let urlStateManagerStub: jasmine.SpyObj; + + beforeEach(() => { + urlStateManagerStub = jasmine.createSpyObj( + "urlStateManagerStub", + [ + "changeBaseUrl", + "locationToURL", + "replaceToUrl", + "navigateToUrl", + "setQueryParam", + ], + ); + }); + + it("should verify instance is created", () => { + // When + const instance = new FiltersSortManager< + FILTER_CRITERIA_UNION, + string, + SORT_CRITERIA_UNION, + string + >(urlStateManagerStub, KNOWN_FILTER_CRITERIA, KNOWN_SORT_CRITERIA); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Properties::", () => { + let manager: FiltersSortManager< + FILTER_CRITERIA_UNION, + string, + SORT_CRITERIA_UNION, + string + >; beforeEach(() => { - urlStateManagerStub = jasmine.createSpyObj('urlStateManagerStub', [ - 'changeBaseUrl', - 'locationToURL', - 'replaceToUrl', - 'navigateToUrl', - 'setQueryParam' + manager = new FiltersSortManager( + urlStateManagerStub, + KNOWN_FILTER_CRITERIA, + KNOWN_SORT_CRITERIA, + ); + }); + + describe("|filterCriteria|", () => { + it("should verify default value is empty object", () => { + // Then + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + }); + }); + + describe("|sortCriteria|", () => { + it("should verify default value is empty object", () => { + // Then + expect(manager.sortCriteria).toEqual( + {} as Record, + ); + }); + }); + }); + + describe("Methods::", () => { + let manager: FiltersSortManager< + FILTER_CRITERIA_UNION, + string, + SORT_CRITERIA_UNION, + string + >; + + beforeEach(() => { + manager = new FiltersSortManager( + urlStateManagerStub, + KNOWN_FILTER_CRITERIA, + KNOWN_SORT_CRITERIA, + ); + }); + + describe("|hasFilter|", () => { + it("should verify will return false when there is not such filter criteria", () => { + // Given + manager.filterCriteria[FILTER_CRITERIA_F1] = "f1v"; + manager.filterCriteria[FILTER_CRITERIA_F2] = "f2v"; + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: "f1v", + [FILTER_CRITERIA_F2]: "f2v", + } as Record); + + // When/Then 2 + expect(manager.hasFilter(FILTER_CRITERIA_F1)).toBeTrue(); + expect(manager.hasFilter(FILTER_CRITERIA_F2)).toBeTrue(); + expect(manager.hasFilter(FILTER_CRITERIA_F3)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F4)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F5)).toBeFalse(); + }); + + it(`should verify will return false when there is such filter criteria but it's value is Nil`, () => { + // Given + manager.filterCriteria[FILTER_CRITERIA_F1] = "f1v"; + manager.filterCriteria[FILTER_CRITERIA_F3] = null; + manager.filterCriteria[FILTER_CRITERIA_F5] = undefined; + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: "f1v", + [FILTER_CRITERIA_F3]: null, + [FILTER_CRITERIA_F5]: undefined, + } as Record); + + // When/Then 2 + expect(manager.hasFilter(FILTER_CRITERIA_F1)).toBeTrue(); + expect(manager.hasFilter(FILTER_CRITERIA_F2)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F3)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F4)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F5)).toBeFalse(); + }); + + it(`should verify will return true when there is such filter criteria and it's value is not Nil`, () => { + // Given + manager.filterCriteria[FILTER_CRITERIA_F2] = "f2v"; + manager.filterCriteria[FILTER_CRITERIA_F3] = "f3v"; + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F2]: "f2v", + [FILTER_CRITERIA_F3]: "f3v", + } as Record); + + // When/Then 2 + expect(manager.hasFilter(FILTER_CRITERIA_F1)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F2)).toBeTrue(); + expect(manager.hasFilter(FILTER_CRITERIA_F3)).toBeTrue(); + expect(manager.hasFilter(FILTER_CRITERIA_F4)).toBeFalse(); + expect(manager.hasFilter(FILTER_CRITERIA_F5)).toBeFalse(); + }); + }); + + describe("|hasAnyFilter|", () => { + it("should verify will return false when there is no filter criteria set", () => { + // Then 1 + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + + // Then 2 + expect(manager.hasAnyFilter()).toBeFalse(); + }); + + it(`should verify will return false when there are two filter criteria but their value is Nil`, () => { + // Given + manager.filterCriteria[FILTER_CRITERIA_F2] = null; + manager.filterCriteria[FILTER_CRITERIA_F4] = undefined; + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F2]: null, + [FILTER_CRITERIA_F4]: undefined, + } as Record); + + // When/Then 2 + expect(manager.hasAnyFilter()).toBeFalse(); + }); + + it(`should verify will return true when there is at least one filter criteria which value is not Nil`, () => { + // Given + manager.filterCriteria[FILTER_CRITERIA_F1] = null; + manager.filterCriteria[FILTER_CRITERIA_F2] = undefined; + manager.filterCriteria[FILTER_CRITERIA_F4] = "f4v"; + manager.filterCriteria[FILTER_CRITERIA_F5] = "f5v"; + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: null, + [FILTER_CRITERIA_F2]: undefined, + [FILTER_CRITERIA_F4]: "f4v", + [FILTER_CRITERIA_F5]: "f5v", + } as Record); + + // When/Then 2 + expect(manager.hasAnyFilter()).toBeTrue(); + }); + }); + + describe("|setFilter|", () => { + it(`should verify won't set filter if it exist and new value is same like stored one`, () => { + // Given + const filterValue1 = "f1v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + } as Record); + + // When + manager.setFilter(FILTER_CRITERIA_F1, filterValue1); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + } as Record); + expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); + expect(observerSpy).not.toHaveBeenCalled(); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + }); + + it(`should verify will set filter if doesn't exist in manager, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // Given + const filterValue1 = "f1v"; + const filterValue2 = "f2v"; + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + + // When + manager.setFilter(FILTER_CRITERIA_F1, filterValue1); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + JSON.stringify({ + [FILTER_CRITERIA_F2]: filterValue2, + [FILTER_CRITERIA_F1]: filterValue1, + }), + ); + + expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will delete filter if exist in manager because provided value is Nil, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // Given + const filterValue3 = "f3v"; + const filterValue4 = "f4v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; + manager.changeUpdateStrategy("replaceToURL"); + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F3]: filterValue3, + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + // When + manager.setFilter(FILTER_CRITERIA_F3, null); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + JSON.stringify({ [FILTER_CRITERIA_F4]: filterValue4 }), + ); + expect(observerSpy).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F3, null, "filter"], + ]); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will delete filters multiple times and update URL by default only once`, fakeAsync(() => { + // Given + const filterValue3 = "f3v"; + const filterValue4 = "f4v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + urlStateManagerStub.navigateToUrl.and.resolveTo(true); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; + manager.changeUpdateStrategy("navigateToUrl"); + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F3]: filterValue3, + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + // When + manager.setFilter(FILTER_CRITERIA_F3, null); + manager.setFilter(FILTER_CRITERIA_F4, null); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + JSON.stringify({ [FILTER_CRITERIA_F4]: filterValue4 }), + ]); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + FILTER_KEY, + null, + ]); + expect(observerSpy.calls.argsFor(0)).toEqual([ + [[FILTER_CRITERIA_F3, null, "filter"]], ]); + expect(observerSpy.calls.argsFor(1)).toEqual([ + [[FILTER_CRITERIA_F4, null, "filter"]], + ]); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); + })); + + it(`should verify won't update URL if updateBrowserUrl parameter is false`, fakeAsync(() => { + // Given + const filterValue4 = "f4v"; + const filterValue5 = "f5v"; + const mutationObserver1 = new MutationObserver(); + const mutationObserver2 = new MutationObserver(); + const observerError = new Error("observer throws error"); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.throwError(observerError); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + // When + manager.setFilter(FILTER_CRITERIA_F5, filterValue5, false); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F4]: filterValue4, + [FILTER_CRITERIA_F5]: filterValue5, + } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + JSON.stringify({ + [FILTER_CRITERIA_F4]: filterValue4, + [FILTER_CRITERIA_F5]: filterValue5, + }), + ); + expect(observerSpy1).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F5, filterValue5, "filter"], + ]); + expect(observerSpy2).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F5, filterValue5, "filter"], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will log error if update URL with navigateToUrl strategy fails`, fakeAsync(() => { + // Given + const filterValue3 = "f3v"; + const navigateToUrlError = new Error("urlUpdateManager throws error"); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + urlStateManagerStub.navigateToUrl.and.rejectWith(navigateToUrlError); + + // prerequisites manager should have + manager.changeUpdateStrategy("navigateToUrl"); + + // Then 1 + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + + // When + manager.setFilter(FILTER_CRITERIA_F3, filterValue3, true); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F3]: filterValue3, + } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + JSON.stringify({ [FILTER_CRITERIA_F3]: filterValue3 }), + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to update Browser Url", + navigateToUrlError, + ); + })); }); - it('should verify instance is created', () => { + describe("|deleteFilter|", () => { + it(`should verify won't delete filter if it doesn't exist`, () => { + // Given + const filterValue4 = "f4v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + // When - const instance = new FiltersSortManager( - urlStateManagerStub, - KNOWN_FILTER_CRITERIA, - KNOWN_SORT_CRITERIA + const returnedValue = manager.deleteFilter(FILTER_CRITERIA_F2); + + // Then 2 + expect(returnedValue).toBeFalse(); + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); + expect(observerSpy).not.toHaveBeenCalled(); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + }); + + it(`should verify will delete filter if it exist in manager, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // Given + const filterValue1 = "f1v"; + const filterValue2 = "f2v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + + // When + const returnedValue = manager.deleteFilter(FILTER_CRITERIA_F1); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(returnedValue).toBeTrue(); + + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2 }), ); + expect(observerSpy).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + ]); - // Then - expect(instance).toBeDefined(); + expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will delete filters multiple times and update URL by default only once`, fakeAsync(() => { + // Given + const filterValue1 = "f1v"; + const filterValue2 = "f2v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + urlStateManagerStub.navigateToUrl.and.resolveTo(true); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; + manager.changeUpdateStrategy("navigateToUrl"); + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + + // When + const returnedValue1 = manager.deleteFilter(FILTER_CRITERIA_F1); + const returnedValue2 = manager.deleteFilter(FILTER_CRITERIA_F2); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(returnedValue1).toBeTrue(); + expect(returnedValue2).toBeTrue(); + + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2 }), + ]); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + FILTER_KEY, + null, + ]); + expect(observerSpy.calls.argsFor(0)).toEqual([ + [[FILTER_CRITERIA_F1, null, "filter"]], + ]); + expect(observerSpy.calls.argsFor(1)).toEqual([ + [[FILTER_CRITERIA_F2, null, "filter"]], + ]); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); + })); + + it(`should verify won't update URL if updateBrowserUrl parameter is false`, fakeAsync(() => { + // Given + const filterValue1 = "f1v"; + const filterValue2 = "f2v"; + const mutationObserver1 = new MutationObserver(); + const mutationObserver2 = new MutationObserver(); + const observerError = new Error("observer throws error"); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.throwError(observerError); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + + // When + const returnedValue = manager.deleteFilter(FILTER_CRITERIA_F1, false); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(returnedValue).toBeTrue(); + + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2 }), + ); + expect(observerSpy1).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + ]); + expect(observerSpy2).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); }); - describe('Properties::', () => { - let manager: FiltersSortManager; + describe("|clearFilters|", () => { + let observerError: Error; + let observerSpy1: jasmine.Spy; + let observerSpy2: jasmine.Spy; + let consoleErrorSpy: jasmine.Spy; + + beforeEach(() => { + const filterValue1 = "f1v"; + const filterValue2 = "f2v"; + + const mutationObserver1 = new MutationObserver(); + const mutationObserver2 = new MutationObserver(); + observerError = new Error("observer throws error"); + observerSpy1 = spyOn(mutationObserver1, "observer").and.throwError( + observerError, + ); + observerSpy2 = spyOn(mutationObserver2, "observer").and.callThrough(); + consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; + manager.filterCriteria[FILTER_CRITERIA_F3] = null; + manager.filterCriteria[FILTER_CRITERIA_F4] = undefined; + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + [FILTER_CRITERIA_F3]: null, + [FILTER_CRITERIA_F4]: undefined, + } as Record); + }); + + it(`should verify will clear all existing filters, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // When + manager.clearFilters(); + manager.clearFilters(); - beforeEach(() => { - manager = new FiltersSortManager(urlStateManagerStub, KNOWN_FILTER_CRITERIA, KNOWN_SORT_CRITERIA); - }); + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - describe('|filterCriteria|', () => { - it('should verify default value is empty object', () => { - // Then - expect(manager.filterCriteria).toEqual({} as Record); - }); - }); + // Then + expect(manager.filterCriteria).toEqual( + {} as Record, + ); - describe('|sortCriteria|', () => { - it('should verify default value is empty object', () => { - // Then - expect(manager.sortCriteria).toEqual({} as Record); - }); - }); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + null, + ); + + expect(observerSpy1.calls.count()).toEqual(1); + expect(observerSpy1.calls.argsFor(0)).toEqual([ + [ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F2, null, "filter"], + [FILTER_CRITERIA_F3, null, "filter"], + [FILTER_CRITERIA_F4, null, "filter"], + ], + ]); + + expect(observerSpy2.calls.count()).toEqual(1); + expect(observerSpy2.calls.argsFor(0)).toEqual([ + [ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F2, null, "filter"], + [FILTER_CRITERIA_F3, null, "filter"], + [FILTER_CRITERIA_F4, null, "filter"], + ], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); + + expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will clear all existing filters, serialize to URLStateManager, and won't update URL and would notify mutation observers`, fakeAsync(() => { + // When + manager.clearFilters(false); + manager.clearFilters(false); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + null, + ); + + expect(observerSpy1.calls.count()).toEqual(1); + expect(observerSpy1).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F2, null, "filter"], + [FILTER_CRITERIA_F3, null, "filter"], + [FILTER_CRITERIA_F4, null, "filter"], + ]); + + expect(observerSpy2.calls.count()).toEqual(1); + expect(observerSpy2).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F2, null, "filter"], + [FILTER_CRITERIA_F3, null, "filter"], + [FILTER_CRITERIA_F4, null, "filter"], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will clear all existing filters, serialize to URLStateManager, update URL by default and won't notify mutation observers`, fakeAsync(() => { + // Given + manager.changeUpdateStrategy("replaceToURL"); + + // When + manager.clearFilters(true, false); + manager.clearFilters(true); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + FILTER_KEY, + null, + ); + + expect(observerSpy1).not.toHaveBeenCalled(); + expect(observerSpy2).not.toHaveBeenCalled(); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); }); - describe('Methods::', () => { - let manager: FiltersSortManager; + describe("|hasSort|", () => { + it("should verify will return false when there is not such sort criteria", () => { + // Given + manager.sortCriteria[SORT_CRITERIA_S1] = "s1v"; + manager.sortCriteria[SORT_CRITERIA_S2] = "s2v"; + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: "s1v", + [SORT_CRITERIA_S2]: "s2v", + } as Record); + + // When/Then 2 + expect(manager.hasSort(SORT_CRITERIA_S1)).toBeTrue(); + expect(manager.hasSort(SORT_CRITERIA_S2)).toBeTrue(); + expect(manager.hasSort(SORT_CRITERIA_S3)).toBeFalse(); + }); + + it(`should verify will return false when there is such sort criteria but it's value is Nil`, () => { + // Given + manager.sortCriteria[SORT_CRITERIA_S1] = "s1v"; + manager.sortCriteria[SORT_CRITERIA_S2] = null; + manager.sortCriteria[SORT_CRITERIA_S3] = undefined; + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: "s1v", + [SORT_CRITERIA_S2]: null, + [SORT_CRITERIA_S3]: undefined, + } as Record); + + // When/Then 2 + expect(manager.hasSort(SORT_CRITERIA_S1)).toBeTrue(); + expect(manager.hasSort(SORT_CRITERIA_S2)).toBeFalse(); + expect(manager.hasSort(SORT_CRITERIA_S3)).toBeFalse(); + }); + + it(`should verify will return true when there is such sort criteria and it's value is not Nil`, () => { + // Given + manager.sortCriteria[SORT_CRITERIA_S1] = "s1v"; + manager.sortCriteria[SORT_CRITERIA_S2] = "s2v"; + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: "s1v", + [SORT_CRITERIA_S2]: "s2v", + } as Record); + + // When/Then 2 + expect(manager.hasSort(SORT_CRITERIA_S1)).toBeTrue(); + expect(manager.hasSort(SORT_CRITERIA_S2)).toBeTrue(); + expect(manager.hasSort(SORT_CRITERIA_S3)).toBeFalse(); + }); + }); - beforeEach(() => { - manager = new FiltersSortManager(urlStateManagerStub, KNOWN_FILTER_CRITERIA, KNOWN_SORT_CRITERIA); - }); + describe("|hasAnySort|", () => { + it("should verify will return false when there is no sort criteria set", () => { + // Then 1 + expect(manager.sortCriteria).toEqual( + {} as Record, + ); - describe('|hasFilter|', () => { - it('should verify will return false when there is not such filter criteria', () => { - // Given - manager.filterCriteria[FILTER_CRITERIA_F1] = 'f1v'; - manager.filterCriteria[FILTER_CRITERIA_F2] = 'f2v'; - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: 'f1v', - [FILTER_CRITERIA_F2]: 'f2v' - } as Record); - - // When/Then 2 - expect(manager.hasFilter(FILTER_CRITERIA_F1)).toBeTrue(); - expect(manager.hasFilter(FILTER_CRITERIA_F2)).toBeTrue(); - expect(manager.hasFilter(FILTER_CRITERIA_F3)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F4)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F5)).toBeFalse(); - }); - - it(`should verify will return false when there is such filter criteria but it's value is Nil`, () => { - // Given - manager.filterCriteria[FILTER_CRITERIA_F1] = 'f1v'; - manager.filterCriteria[FILTER_CRITERIA_F3] = null; - manager.filterCriteria[FILTER_CRITERIA_F5] = undefined; - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: 'f1v', - [FILTER_CRITERIA_F3]: null, - [FILTER_CRITERIA_F5]: undefined - } as Record); - - // When/Then 2 - expect(manager.hasFilter(FILTER_CRITERIA_F1)).toBeTrue(); - expect(manager.hasFilter(FILTER_CRITERIA_F2)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F3)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F4)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F5)).toBeFalse(); - }); - - it(`should verify will return true when there is such filter criteria and it's value is not Nil`, () => { - // Given - manager.filterCriteria[FILTER_CRITERIA_F2] = 'f2v'; - manager.filterCriteria[FILTER_CRITERIA_F3] = 'f3v'; - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F2]: 'f2v', - [FILTER_CRITERIA_F3]: 'f3v' - } as Record); - - // When/Then 2 - expect(manager.hasFilter(FILTER_CRITERIA_F1)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F2)).toBeTrue(); - expect(manager.hasFilter(FILTER_CRITERIA_F3)).toBeTrue(); - expect(manager.hasFilter(FILTER_CRITERIA_F4)).toBeFalse(); - expect(manager.hasFilter(FILTER_CRITERIA_F5)).toBeFalse(); - }); - }); + // Then 2 + expect(manager.hasAnySort()).toBeFalse(); + }); + + it(`should verify will return false when there are two sort criteria but their value is Nil`, () => { + // Given + manager.sortCriteria[SORT_CRITERIA_S1] = null; + manager.sortCriteria[SORT_CRITERIA_S2] = undefined; + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: null, + [SORT_CRITERIA_S2]: undefined, + } as Record); + + // When/Then 2 + expect(manager.hasAnySort()).toBeFalse(); + }); + + it(`should verify will return true when there is at least one sort criteria which value is not Nil`, () => { + // Given + manager.sortCriteria[SORT_CRITERIA_S1] = null; + manager.sortCriteria[SORT_CRITERIA_S2] = undefined; + manager.sortCriteria[SORT_CRITERIA_S3] = "s3v"; + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: null, + [SORT_CRITERIA_S2]: undefined, + [SORT_CRITERIA_S3]: "s3v", + } as Record); + + // When/Then 2 + expect(manager.hasAnySort()).toBeTrue(); + }); + }); - describe('|hasAnyFilter|', () => { - it('should verify will return false when there is no filter criteria set', () => { - // Then 1 - expect(manager.filterCriteria).toEqual({} as Record); - - // Then 2 - expect(manager.hasAnyFilter()).toBeFalse(); - }); - - it(`should verify will return false when there are two filter criteria but their value is Nil`, () => { - // Given - manager.filterCriteria[FILTER_CRITERIA_F2] = null; - manager.filterCriteria[FILTER_CRITERIA_F4] = undefined; - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F2]: null, - [FILTER_CRITERIA_F4]: undefined - } as Record); - - // When/Then 2 - expect(manager.hasAnyFilter()).toBeFalse(); - }); - - it(`should verify will return true when there is at least one filter criteria which value is not Nil`, () => { - // Given - manager.filterCriteria[FILTER_CRITERIA_F1] = null; - manager.filterCriteria[FILTER_CRITERIA_F2] = undefined; - manager.filterCriteria[FILTER_CRITERIA_F4] = 'f4v'; - manager.filterCriteria[FILTER_CRITERIA_F5] = 'f5v'; - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: null, - [FILTER_CRITERIA_F2]: undefined, - [FILTER_CRITERIA_F4]: 'f4v', - [FILTER_CRITERIA_F5]: 'f5v' - } as Record); - - // When/Then 2 - expect(manager.hasAnyFilter()).toBeTrue(); - }); - }); + describe("|setSort|", () => { + it(`should verify won't set sort if it exist and new value is same like stored one`, () => { + // Given + const sortValue1 = "s1v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + } as Record); - describe('|setFilter|', () => { - it(`should verify won't set filter if it exist and new value is same like stored one`, () => { - // Given - const filterValue1 = 'f1v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F1]: filterValue1 } as Record); - - // When - manager.setFilter(FILTER_CRITERIA_F1, filterValue1); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1 - } as Record); - expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); - expect(observerSpy).not.toHaveBeenCalled(); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - }); - - it(`should verify will set filter if doesn't exist in manager, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v'; - const filterValue2 = 'f2v'; - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; - - // Then 1 - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F2]: filterValue2 } as Record); - - // When - manager.setFilter(FILTER_CRITERIA_F1, filterValue1); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2 - } as Record); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2, [FILTER_CRITERIA_F1]: filterValue1 }) - ); - - expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will delete filter if exist in manager because provided value is Nil, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // Given - const filterValue3 = 'f3v'; - const filterValue4 = 'f4v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; - manager.changeUpdateStrategy('replaceToURL'); - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F3]: filterValue3, - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - - // When - manager.setFilter(FILTER_CRITERIA_F3, null); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F4]: filterValue4 }) - ); - expect(observerSpy).toHaveBeenCalledWith([[FILTER_CRITERIA_F3, null, 'filter']]); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will delete filters multiple times and update URL by default only once`, fakeAsync(() => { - // Given - const filterValue3 = 'f3v'; - const filterValue4 = 'f4v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - urlStateManagerStub.navigateToUrl.and.resolveTo(true); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; - manager.changeUpdateStrategy('navigateToUrl'); - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F3]: filterValue3, - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - - // When - manager.setFilter(FILTER_CRITERIA_F3, null); - manager.setFilter(FILTER_CRITERIA_F4, null); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({} as Record); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F4]: filterValue4 }) - ]); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([FILTER_KEY, null]); - expect(observerSpy.calls.argsFor(0)).toEqual([[[FILTER_CRITERIA_F3, null, 'filter']]]); - expect(observerSpy.calls.argsFor(1)).toEqual([[[FILTER_CRITERIA_F4, null, 'filter']]]); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); - })); - - it(`should verify won't update URL if updateBrowserUrl parameter is false`, fakeAsync(() => { - // Given - const filterValue4 = 'f4v'; - const filterValue5 = 'f5v'; - const mutationObserver1 = new MutationObserver(); - const mutationObserver2 = new MutationObserver(); - const observerError = new Error('observer throws error'); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F4]: filterValue4 } as Record); - - // When - manager.setFilter(FILTER_CRITERIA_F5, filterValue5, false); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F4]: filterValue4, - [FILTER_CRITERIA_F5]: filterValue5 - } as Record); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F4]: filterValue4, [FILTER_CRITERIA_F5]: filterValue5 }) - ); - expect(observerSpy1).toHaveBeenCalledWith([[FILTER_CRITERIA_F5, filterValue5, 'filter']]); - expect(observerSpy2).toHaveBeenCalledWith([[FILTER_CRITERIA_F5, filterValue5, 'filter']]); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will log error if update URL with navigateToUrl strategy fails`, fakeAsync(() => { - // Given - const filterValue3 = 'f3v'; - const navigateToUrlError = new Error('urlUpdateManager throws error'); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - urlStateManagerStub.navigateToUrl.and.rejectWith(navigateToUrlError); - - // prerequisites manager should have - manager.changeUpdateStrategy('navigateToUrl'); - - // Then 1 - expect(manager.filterCriteria).toEqual({} as Record); - - // When - manager.setFilter(FILTER_CRITERIA_F3, filterValue3, true); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F3]: filterValue3 } as Record); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F3]: filterValue3 }) - ); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to update Browser Url', navigateToUrlError); - })); - }); + // When + manager.setSort(SORT_CRITERIA_S1, sortValue1); - describe('|deleteFilter|', () => { - it(`should verify won't delete filter if it doesn't exist`, () => { - // Given - const filterValue4 = 'f4v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F4] = filterValue4; - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F4]: filterValue4 } as Record); - - // When - const returnedValue = manager.deleteFilter(FILTER_CRITERIA_F2); - - // Then 2 - expect(returnedValue).toBeFalse(); - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - - expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); - expect(observerSpy).not.toHaveBeenCalled(); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - }); - - it(`should verify will delete filter if it exist in manager, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v'; - const filterValue2 = 'f2v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2 - } as Record); - - // When - const returnedValue = manager.deleteFilter(FILTER_CRITERIA_F1); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(returnedValue).toBeTrue(); - - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F2]: filterValue2 } as Record); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2 }) - ); - expect(observerSpy).toHaveBeenCalledWith([[FILTER_CRITERIA_F1, null, 'filter']]); - - expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will delete filters multiple times and update URL by default only once`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v'; - const filterValue2 = 'f2v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - urlStateManagerStub.navigateToUrl.and.resolveTo(true); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; - manager.changeUpdateStrategy('navigateToUrl'); - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2 - } as Record); - - // When - const returnedValue1 = manager.deleteFilter(FILTER_CRITERIA_F1); - const returnedValue2 = manager.deleteFilter(FILTER_CRITERIA_F2); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(returnedValue1).toBeTrue(); - expect(returnedValue2).toBeTrue(); - - expect(manager.filterCriteria).toEqual({} as Record); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2 }) - ]); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([FILTER_KEY, null]); - expect(observerSpy.calls.argsFor(0)).toEqual([[[FILTER_CRITERIA_F1, null, 'filter']]]); - expect(observerSpy.calls.argsFor(1)).toEqual([[[FILTER_CRITERIA_F2, null, 'filter']]]); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); - })); - - it(`should verify won't update URL if updateBrowserUrl parameter is false`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v'; - const filterValue2 = 'f2v'; - const mutationObserver1 = new MutationObserver(); - const mutationObserver2 = new MutationObserver(); - const observerError = new Error('observer throws error'); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2 - } as Record); - - // When - const returnedValue = manager.deleteFilter(FILTER_CRITERIA_F1, false); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then 2 - expect(returnedValue).toBeTrue(); - - expect(manager.filterCriteria).toEqual({ [FILTER_CRITERIA_F2]: filterValue2 } as Record); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F2]: filterValue2 }) - ); - expect(observerSpy1).toHaveBeenCalledWith([[FILTER_CRITERIA_F1, null, 'filter']]); - expect(observerSpy2).toHaveBeenCalledWith([[FILTER_CRITERIA_F1, null, 'filter']]); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - }); + // Then 2 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + } as Record); - describe('|clearFilters|', () => { - let observerError: Error; - let observerSpy1: jasmine.Spy; - let observerSpy2: jasmine.Spy; - let consoleErrorSpy: jasmine.Spy; - - beforeEach(() => { - const filterValue1 = 'f1v'; - const filterValue2 = 'f2v'; - - const mutationObserver1 = new MutationObserver(); - const mutationObserver2 = new MutationObserver(); - observerError = new Error('observer throws error'); - observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; - manager.filterCriteria[FILTER_CRITERIA_F3] = null; - manager.filterCriteria[FILTER_CRITERIA_F4] = undefined; - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2, - [FILTER_CRITERIA_F3]: null, - [FILTER_CRITERIA_F4]: undefined - } as Record); - }); - - it(`should verify will clear all existing filters, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // When - manager.clearFilters(); - manager.clearFilters(); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then - expect(manager.filterCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith(FILTER_KEY, null); - - expect(observerSpy1.calls.count()).toEqual(1); - expect(observerSpy1.calls.argsFor(0)).toEqual([ - [ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F2, null, 'filter'], - [FILTER_CRITERIA_F3, null, 'filter'], - [FILTER_CRITERIA_F4, null, 'filter'] - ] - ]); - - expect(observerSpy2.calls.count()).toEqual(1); - expect(observerSpy2.calls.argsFor(0)).toEqual([ - [ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F2, null, 'filter'], - [FILTER_CRITERIA_F3, null, 'filter'], - [FILTER_CRITERIA_F4, null, 'filter'] - ] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); - - expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will clear all existing filters, serialize to URLStateManager, and won't update URL and would notify mutation observers`, fakeAsync(() => { - // When - manager.clearFilters(false); - manager.clearFilters(false); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then - expect(manager.filterCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith(FILTER_KEY, null); - - expect(observerSpy1.calls.count()).toEqual(1); - expect(observerSpy1).toHaveBeenCalledWith([ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F2, null, 'filter'], - [FILTER_CRITERIA_F3, null, 'filter'], - [FILTER_CRITERIA_F4, null, 'filter'] - ]); - - expect(observerSpy2.calls.count()).toEqual(1); - expect(observerSpy2).toHaveBeenCalledWith([ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F2, null, 'filter'], - [FILTER_CRITERIA_F3, null, 'filter'], - [FILTER_CRITERIA_F4, null, 'filter'] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will clear all existing filters, serialize to URLStateManager, update URL by default and won't notify mutation observers`, fakeAsync(() => { - // Given - manager.changeUpdateStrategy('replaceToURL'); - - // When - manager.clearFilters(true, false); - manager.clearFilters(true); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then - expect(manager.filterCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith(FILTER_KEY, null); - - expect(observerSpy1).not.toHaveBeenCalled(); - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - }); + expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); - describe('|hasSort|', () => { - it('should verify will return false when there is not such sort criteria', () => { - // Given - manager.sortCriteria[SORT_CRITERIA_S1] = 's1v'; - manager.sortCriteria[SORT_CRITERIA_S2] = 's2v'; - - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: 's1v', - [SORT_CRITERIA_S2]: 's2v' - } as Record); - - // When/Then 2 - expect(manager.hasSort(SORT_CRITERIA_S1)).toBeTrue(); - expect(manager.hasSort(SORT_CRITERIA_S2)).toBeTrue(); - expect(manager.hasSort(SORT_CRITERIA_S3)).toBeFalse(); - }); - - it(`should verify will return false when there is such sort criteria but it's value is Nil`, () => { - // Given - manager.sortCriteria[SORT_CRITERIA_S1] = 's1v'; - manager.sortCriteria[SORT_CRITERIA_S2] = null; - manager.sortCriteria[SORT_CRITERIA_S3] = undefined; - - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: 's1v', - [SORT_CRITERIA_S2]: null, - [SORT_CRITERIA_S3]: undefined - } as Record); - - // When/Then 2 - expect(manager.hasSort(SORT_CRITERIA_S1)).toBeTrue(); - expect(manager.hasSort(SORT_CRITERIA_S2)).toBeFalse(); - expect(manager.hasSort(SORT_CRITERIA_S3)).toBeFalse(); - }); - - it(`should verify will return true when there is such sort criteria and it's value is not Nil`, () => { - // Given - manager.sortCriteria[SORT_CRITERIA_S1] = 's1v'; - manager.sortCriteria[SORT_CRITERIA_S2] = 's2v'; - - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: 's1v', - [SORT_CRITERIA_S2]: 's2v' - } as Record); - - // When/Then 2 - expect(manager.hasSort(SORT_CRITERIA_S1)).toBeTrue(); - expect(manager.hasSort(SORT_CRITERIA_S2)).toBeTrue(); - expect(manager.hasSort(SORT_CRITERIA_S3)).toBeFalse(); - }); - }); + expect(observerSpy).not.toHaveBeenCalled(); - describe('|hasAnySort|', () => { - it('should verify will return false when there is no sort criteria set', () => { - // Then 1 - expect(manager.sortCriteria).toEqual({} as Record); - - // Then 2 - expect(manager.hasAnySort()).toBeFalse(); - }); - - it(`should verify will return false when there are two sort criteria but their value is Nil`, () => { - // Given - manager.sortCriteria[SORT_CRITERIA_S1] = null; - manager.sortCriteria[SORT_CRITERIA_S2] = undefined; - - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: null, - [SORT_CRITERIA_S2]: undefined - } as Record); - - // When/Then 2 - expect(manager.hasAnySort()).toBeFalse(); - }); - - it(`should verify will return true when there is at least one sort criteria which value is not Nil`, () => { - // Given - manager.sortCriteria[SORT_CRITERIA_S1] = null; - manager.sortCriteria[SORT_CRITERIA_S2] = undefined; - manager.sortCriteria[SORT_CRITERIA_S3] = 's3v'; - - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: null, - [SORT_CRITERIA_S2]: undefined, - [SORT_CRITERIA_S3]: 's3v' - } as Record); - - // When/Then 2 - expect(manager.hasAnySort()).toBeTrue(); - }); - }); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + }); - describe('|setSort|', () => { - it(`should verify won't set sort if it exist and new value is same like stored one`, () => { - // Given - const sortValue1 = 's1v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - // prerequisites manager should have - manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; - manager.registerMutationObserver(mutationObserver.observer); + it(`should verify will set sort if doesn't exist in manager, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // Given + const sortValue1 = "s1v"; + const sortValue2 = "s2v"; - // Then 1 - expect(manager.sortCriteria).toEqual({ [SORT_CRITERIA_S1]: sortValue1 } as Record); + // prerequisites manager should have + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - // When - manager.setSort(SORT_CRITERIA_S1, sortValue1); + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + } as Record); - // Then 2 - expect(manager.sortCriteria).toEqual({ [SORT_CRITERIA_S1]: sortValue1 } as Record); + // When + manager.setSort(SORT_CRITERIA_S1, sortValue1); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + } as Record); + + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + JSON.stringify({ + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S1]: sortValue1, + }), + ); - expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); + expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will delete sort if exist in manager because provided value is Nil, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // Given + const sortValue1 = "s1v"; + const sortValue2 = "s2v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + manager.changeUpdateStrategy("replaceToURL"); + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + } as Record); - expect(observerSpy).not.toHaveBeenCalled(); + // When + manager.setSort(SORT_CRITERIA_S1, null); - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - }); + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - it(`should verify will set sort if doesn't exist in manager, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // Given - const sortValue1 = 's1v'; - const sortValue2 = 's2v'; + // Then 2 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + } as Record); - // prerequisites manager should have - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + JSON.stringify({ [SORT_CRITERIA_S2]: sortValue2 }), + ); - // Then 1 - expect(manager.sortCriteria).toEqual({ [SORT_CRITERIA_S2]: sortValue2 } as Record); + expect(observerSpy).toHaveBeenCalledWith([ + [SORT_CRITERIA_S1, null, "sort"], + ]); - // When - manager.setSort(SORT_CRITERIA_S1, sortValue1); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); + it(`should verify will delete sort multiple times and update URL by default only once`, fakeAsync(() => { + // Given + const sortValue1 = "s1v"; + const sortValue2 = "s2v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); - // Then 2 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: sortValue2 - } as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S2]: sortValue2, [SORT_CRITERIA_S1]: sortValue1 }) - ); + urlStateManagerStub.navigateToUrl.and.resolveTo(true); - expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); + // prerequisites manager should have + manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - it(`should verify will delete sort if exist in manager because provided value is Nil, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // Given - const sortValue1 = 's1v'; - const sortValue2 = 's2v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); + manager.changeUpdateStrategy("navigateToUrl"); + manager.registerMutationObserver(mutationObserver.observer); - // prerequisites manager should have - manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - manager.changeUpdateStrategy('replaceToURL'); - manager.registerMutationObserver(mutationObserver.observer); + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + } as Record); - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: sortValue2 - } as Record); + // When + manager.setSort(SORT_CRITERIA_S1, null); + manager.setSort(SORT_CRITERIA_S2, null); - // When - manager.setSort(SORT_CRITERIA_S1, null); + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); + // Then 2 + expect(manager.sortCriteria).toEqual( + {} as Record, + ); - // Then 2 - expect(manager.sortCriteria).toEqual({ [SORT_CRITERIA_S2]: sortValue2 } as Record); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + SORT_KEY, + JSON.stringify({ [SORT_CRITERIA_S2]: sortValue2 }), + ]); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + SORT_KEY, + null, + ]); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S2]: sortValue2 }) - ); + expect(observerSpy.calls.argsFor(0)).toEqual([ + [[SORT_CRITERIA_S1, null, "sort"]], + ]); + expect(observerSpy.calls.argsFor(1)).toEqual([ + [[SORT_CRITERIA_S2, null, "sort"]], + ]); - expect(observerSpy).toHaveBeenCalledWith([[SORT_CRITERIA_S1, null, 'sort']]); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); + })); - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); + it(`should verify won't update URL if updateBrowserUrl parameter is false`, fakeAsync(() => { + // Given + const sortValue1 = "s1v"; + const sortValue2 = "s2v"; - it(`should verify will delete sort multiple times and update URL by default only once`, fakeAsync(() => { - // Given - const sortValue1 = 's1v'; - const sortValue2 = 's2v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); + const mutationObserver1 = new MutationObserver(); + const mutationObserver2 = new MutationObserver(); - urlStateManagerStub.navigateToUrl.and.resolveTo(true); + const observerError = new Error("observer throws error"); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.throwError(observerError); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); - // prerequisites manager should have - manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); - manager.changeUpdateStrategy('navigateToUrl'); - manager.registerMutationObserver(mutationObserver.observer); + // prerequisites manager should have + manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: sortValue2 - } as Record); + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); - // When - manager.setSort(SORT_CRITERIA_S1, null); - manager.setSort(SORT_CRITERIA_S2, null); + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + } as Record); - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); + // When + manager.setSort(SORT_CRITERIA_S2, sortValue2, false); + + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); + + // Then 2 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + } as Record); + + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + JSON.stringify({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + }), + ); - // Then 2 - expect(manager.sortCriteria).toEqual({} as Record); + expect(observerSpy1).toHaveBeenCalledWith([ + [SORT_CRITERIA_S2, sortValue2, "sort"], + ]); + expect(observerSpy2).toHaveBeenCalledWith([ + [SORT_CRITERIA_S2, sortValue2, "sort"], + ]); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S2]: sortValue2 }) - ]); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([SORT_KEY, null]); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); - expect(observerSpy.calls.argsFor(0)).toEqual([[[SORT_CRITERIA_S1, null, 'sort']]]); - expect(observerSpy.calls.argsFor(1)).toEqual([[[SORT_CRITERIA_S2, null, 'sort']]]); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); - })); + it(`should verify will log error if update URL with navigateToUrl strategy fails`, fakeAsync(() => { + // Given + const sortValue1 = "s1v"; + const navigateToUrlError = new Error("urlUpdateManager throws error"); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); - it(`should verify won't update URL if updateBrowserUrl parameter is false`, fakeAsync(() => { - // Given - const sortValue1 = 's1v'; - const sortValue2 = 's2v'; + urlStateManagerStub.navigateToUrl.and.rejectWith(navigateToUrlError); - const mutationObserver1 = new MutationObserver(); - const mutationObserver2 = new MutationObserver(); + // prerequisites manager should have + manager.changeUpdateStrategy("navigateToUrl"); - const observerError = new Error('observer throws error'); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); + // Then 1 + expect(manager.sortCriteria).toEqual( + {} as Record, + ); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + // When + manager.setSort(SORT_CRITERIA_S1, sortValue1, true); - // prerequisites manager should have - manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); + // Then 2 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + } as Record); - // Then 1 - expect(manager.sortCriteria).toEqual({ [SORT_CRITERIA_S1]: sortValue1 } as Record); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + JSON.stringify({ [SORT_CRITERIA_S1]: sortValue1 }), + ); - // When - manager.setSort(SORT_CRITERIA_S2, sortValue2, false); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to update Browser Url", + navigateToUrlError, + ); + })); + }); - // Then 2 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: sortValue2 - } as Record); + describe("|clearSort|", () => { + let observerError: Error; + let observerSpy1: jasmine.Spy; + let observerSpy2: jasmine.Spy; + let consoleErrorSpy: jasmine.Spy; - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S1]: sortValue1, [SORT_CRITERIA_S2]: sortValue2 }) - ); + beforeEach(() => { + const sortValue1 = "s1v"; - expect(observerSpy1).toHaveBeenCalledWith([[SORT_CRITERIA_S2, sortValue2, 'sort']]); - expect(observerSpy2).toHaveBeenCalledWith([[SORT_CRITERIA_S2, sortValue2, 'sort']]); + const mutationObserver1 = new MutationObserver(); + const mutationObserver2 = new MutationObserver(); + observerError = new Error("observer throws error"); + observerSpy1 = spyOn(mutationObserver1, "observer").and.throwError( + observerError, + ); + observerSpy2 = spyOn(mutationObserver2, "observer").and.callThrough(); + consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; + manager.sortCriteria[SORT_CRITERIA_S2] = null; + manager.sortCriteria[SORT_CRITERIA_S3] = undefined; + + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: null, + [SORT_CRITERIA_S3]: undefined, + } as Record); + }); + + it(`should verify will clear all existing sort, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { + // When + manager.clearSort(); + manager.clearSort(); - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); + // Then + expect(manager.sortCriteria).toEqual( + {} as Record, + ); - it(`should verify will log error if update URL with navigateToUrl strategy fails`, fakeAsync(() => { - // Given - const sortValue1 = 's1v'; - const navigateToUrlError = new Error('urlUpdateManager throws error'); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + null, + ); - urlStateManagerStub.navigateToUrl.and.rejectWith(navigateToUrlError); + expect(observerSpy1.calls.count()).toEqual(1); + expect(observerSpy1.calls.argsFor(0)).toEqual([ + [ + [SORT_CRITERIA_S1, null, "sort"], + [SORT_CRITERIA_S2, null, "sort"], + [SORT_CRITERIA_S3, null, "sort"], + ], + ]); - // prerequisites manager should have - manager.changeUpdateStrategy('navigateToUrl'); + expect(observerSpy2.calls.count()).toEqual(1); + expect(observerSpy2.calls.argsFor(0)).toEqual([ + [ + [SORT_CRITERIA_S1, null, "sort"], + [SORT_CRITERIA_S2, null, "sort"], + [SORT_CRITERIA_S3, null, "sort"], + ], + ]); - // Then 1 - expect(manager.sortCriteria).toEqual({} as Record); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); - // When - manager.setSort(SORT_CRITERIA_S1, sortValue1, true); + expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); + it(`should verify will clear all existing sort, serialize to URLStateManager, and won't update URL and would notify mutation observers`, fakeAsync(() => { + // When + manager.clearSort(false); + manager.clearSort(false); - // Then 2 - expect(manager.sortCriteria).toEqual({ [SORT_CRITERIA_S1]: sortValue1 } as Record); + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S1]: sortValue1 }) - ); + // Then + expect(manager.sortCriteria).toEqual( + {} as Record, + ); - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).toHaveBeenCalledTimes(1); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + null, + ); - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to update Browser Url', navigateToUrlError); - })); - }); + expect(observerSpy1.calls.count()).toEqual(1); + expect(observerSpy1).toHaveBeenCalledWith([ + [SORT_CRITERIA_S1, null, "sort"], + [SORT_CRITERIA_S2, null, "sort"], + [SORT_CRITERIA_S3, null, "sort"], + ]); - describe('|clearSort|', () => { - let observerError: Error; - let observerSpy1: jasmine.Spy; - let observerSpy2: jasmine.Spy; - let consoleErrorSpy: jasmine.Spy; - - beforeEach(() => { - const sortValue1 = 's1v'; - - const mutationObserver1 = new MutationObserver(); - const mutationObserver2 = new MutationObserver(); - observerError = new Error('observer throws error'); - observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; - manager.sortCriteria[SORT_CRITERIA_S2] = null; - manager.sortCriteria[SORT_CRITERIA_S3] = undefined; - - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: null, - [SORT_CRITERIA_S3]: undefined - } as Record); - }); - - it(`should verify will clear all existing sort, serialize to URLStateManager, update URL by default and notify mutation observers`, fakeAsync(() => { - // When - manager.clearSort(); - manager.clearSort(); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then - expect(manager.sortCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith(SORT_KEY, null); - - expect(observerSpy1.calls.count()).toEqual(1); - expect(observerSpy1.calls.argsFor(0)).toEqual([ - [ - [SORT_CRITERIA_S1, null, 'sort'], - [SORT_CRITERIA_S2, null, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'] - ] - ]); - - expect(observerSpy2.calls.count()).toEqual(1); - expect(observerSpy2.calls.argsFor(0)).toEqual([ - [ - [SORT_CRITERIA_S1, null, 'sort'], - [SORT_CRITERIA_S2, null, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'] - ] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); - - expect(urlStateManagerStub.locationToURL).toHaveBeenCalledTimes(1); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will clear all existing sort, serialize to URLStateManager, and won't update URL and would notify mutation observers`, fakeAsync(() => { - // When - manager.clearSort(false); - manager.clearSort(false); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then - expect(manager.sortCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith(SORT_KEY, null); - - expect(observerSpy1.calls.count()).toEqual(1); - expect(observerSpy1).toHaveBeenCalledWith([ - [SORT_CRITERIA_S1, null, 'sort'], - [SORT_CRITERIA_S2, null, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'] - ]); - - expect(observerSpy2.calls.count()).toEqual(1); - expect(observerSpy2).toHaveBeenCalledWith([ - [SORT_CRITERIA_S1, null, 'sort'], - [SORT_CRITERIA_S2, null, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith('FiltersManager: Failed to notify mutation observers', observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will clear all existing sort, serialize to URLStateManager, update URL by default and won't notify mutation observers`, fakeAsync(() => { - // Given - manager.changeUpdateStrategy('replaceToURL'); - - // When - manager.clearSort(true, false); - manager.clearSort(true); - - // wait virtually 1s because navigation is debounced for 300ms by default - tick(1000); - - // Then - expect(manager.sortCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith(SORT_KEY, null); - - expect(observerSpy1).not.toHaveBeenCalled(); - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - }); + expect(observerSpy2.calls.count()).toEqual(1); + expect(observerSpy2).toHaveBeenCalledWith([ + [SORT_CRITERIA_S1, null, "sort"], + [SORT_CRITERIA_S2, null, "sort"], + [SORT_CRITERIA_S3, null, "sort"], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + "FiltersManager: Failed to notify mutation observers", + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will clear all existing sort, serialize to URLStateManager, update URL by default and won't notify mutation observers`, fakeAsync(() => { + // Given + manager.changeUpdateStrategy("replaceToURL"); - describe('|clear|', () => { - it('should verify will invoke correct methods with correct parameters', () => { - // Given - const clearFiltersSpy = spyOn(manager, 'clearFilters').and.callFake(CallFake); - const clearSortSpy = spyOn(manager, 'clearSort').and.callFake(CallFake); + // When + manager.clearSort(true, false); + manager.clearSort(true); - // When 1 - manager.clear(); + // wait virtually 1s because navigation is debounced for 300ms by default + tick(1000); - // When 2 - manager.clear(true, true); + // Then + expect(manager.sortCriteria).toEqual( + {} as Record, + ); - // When 3 - manager.clear(false, false); + expect(urlStateManagerStub.setQueryParam).toHaveBeenCalledWith( + SORT_KEY, + null, + ); - // Then - expect(clearFiltersSpy.calls.argsFor(0)).toEqual([true, true]); - expect(clearSortSpy.calls.argsFor(0)).toEqual([true, true]); + expect(observerSpy1).not.toHaveBeenCalled(); + expect(observerSpy2).not.toHaveBeenCalled(); - expect(clearFiltersSpy.calls.argsFor(1)).toEqual([true, true]); - expect(clearSortSpy.calls.argsFor(1)).toEqual([true, true]); + expect(consoleErrorSpy).not.toHaveBeenCalled(); - expect(clearFiltersSpy.calls.argsFor(2)).toEqual([false, false]); - expect(clearSortSpy.calls.argsFor(2)).toEqual([false, false]); - }); - }); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + }); + + describe("|clear|", () => { + it("should verify will invoke correct methods with correct parameters", () => { + // Given + const clearFiltersSpy = spyOn(manager, "clearFilters").and.callFake( + CallFake, + ); + const clearSortSpy = spyOn(manager, "clearSort").and.callFake(CallFake); + + // When 1 + manager.clear(); + + // When 2 + manager.clear(true, true); + + // When 3 + manager.clear(false, false); + + // Then + expect(clearFiltersSpy.calls.argsFor(0)).toEqual([true, true]); + expect(clearSortSpy.calls.argsFor(0)).toEqual([true, true]); + + expect(clearFiltersSpy.calls.argsFor(1)).toEqual([true, true]); + expect(clearSortSpy.calls.argsFor(1)).toEqual([true, true]); + + expect(clearFiltersSpy.calls.argsFor(2)).toEqual([false, false]); + expect(clearSortSpy.calls.argsFor(2)).toEqual([false, false]); + }); + }); + + describe("|bulkUpdate|", () => { + it(`should verify if value is Nil won't execute anything`, fakeAsync(() => { + // Given + const filterValue1 = "f1v"; + const filterValue2 = "f2v"; + const sortValue1 = "s1v"; + const sortValue2 = "s2v"; + const mutationObserver = new MutationObserver(); + const observerSpy = spyOn( + mutationObserver, + "observer", + ).and.callThrough(); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; + + manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + + manager.registerMutationObserver(mutationObserver.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + } as Record); + + // When + manager.bulkUpdate(null); + + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F2]: filterValue2, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S1]: sortValue1, + [SORT_CRITERIA_S2]: sortValue2, + } as Record); + + expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); + + expect(observerSpy).not.toHaveBeenCalled(); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will update values from Array of key-value (criteria-value) tuples, filter and sort`, fakeAsync(() => { + // Given + const filterValue1 = "f1v,f11v"; + const filterValue3 = "f3v"; + const filterValue4 = "f4v,f44v,f444v"; + const sortValue1 = "s1v"; + const sortValue2 = "-1"; + const sortValue3 = "s3v"; + const sortValue4 = "s4v"; + + const observerError = new Error("observer throws error"); + + const mutationObserver1 = new MutationObserver(); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.callThrough(); + const mutationObserver2 = new MutationObserver(); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const mutationObserver3 = new MutationObserver(); + const observerSpy3 = spyOn( + mutationObserver3, + "observer", + ).and.throwError(observerError); + + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; + manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; + + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + manager.registerMutationObserver(mutationObserver3.observer); + + manager.deleteMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: filterValue3, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + } as Record); + + // When + manager.bulkUpdate([ + [SORT_CRITERIA_S1, sortValue1, "sort"], + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F3, filterValue3, "filter"], + [SORT_CRITERIA_S3, null, "sort"], + [FILTER_CRITERIA_F2, null, "filter"], + ["someSort", "-1", "sort"] as unknown as KeyValueTuple< + SORT_CRITERIA_UNION, + string + >, + [FILTER_CRITERIA_F4, filterValue4, "filter"], + ["someFilter", "testValue", "filter"] as unknown as KeyValueTuple< + FILTER_CRITERIA_UNION, + string + >, + [SORT_CRITERIA_S2, sortValue2, "sort"], + [SORT_CRITERIA_S4, "", "sort"], + ]); + + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F3]: filterValue3, + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, + [SORT_CRITERIA_S1]: sortValue1, + } as Record); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + JSON.stringify({ + [FILTER_CRITERIA_F3]: filterValue3, + [FILTER_CRITERIA_F4]: filterValue4, + }), + ]); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + SORT_KEY, + JSON.stringify({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), + [SORT_CRITERIA_S1]: sortValue1, + }), + ]); + + expect(observerSpy1).toHaveBeenCalledTimes(1); + expect(observerSpy1).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F4, filterValue4, "filter"], + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ]); + + expect(observerSpy2).not.toHaveBeenCalled(); + + expect(observerSpy3).toHaveBeenCalledTimes(1); + expect(observerSpy3).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F4, filterValue4, "filter"], + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `FiltersManager: Failed to notify mutation observers`, + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will update values from Array of key-value (criteria-value) tuples, filter and sort and clear previous values`, fakeAsync(() => { + // Given + const filterValue1 = "f1v,f11v"; + const filterValue3 = "f3v"; + const filterValue4 = "f4v,f44v,f444v"; + const sortValue1 = "s1v"; + const sortValue2 = "-1"; + const sortValue3 = "s3v"; + const sortValue4 = "s4v"; + + const observerError = new Error("observer throws error"); + + const mutationObserver1 = new MutationObserver(); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.callThrough(); + const mutationObserver2 = new MutationObserver(); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const mutationObserver3 = new MutationObserver(); + const observerSpy3 = spyOn( + mutationObserver3, + "observer", + ).and.throwError(observerError); + + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; + manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; + + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + manager.registerMutationObserver(mutationObserver3.observer); + + manager.deleteMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: filterValue3, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + } as Record); + + // When + manager.bulkUpdate( + [ + [SORT_CRITERIA_S1, sortValue1, "sort"], + [FILTER_CRITERIA_F3, filterValue3, "filter"], + [FILTER_CRITERIA_F2, null, "filter"], + ["someSort", "-1", "sort"] as unknown as KeyValueTuple< + SORT_CRITERIA_UNION, + string + >, + [FILTER_CRITERIA_F4, filterValue4, "filter"], + ["someFilter", "testValue", "filter"] as unknown as KeyValueTuple< + FILTER_CRITERIA_UNION, + string + >, + [SORT_CRITERIA_S2, sortValue2, "sort"], + [SORT_CRITERIA_S4, "", "sort"], + ], + true, + ); + + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F3]: filterValue3, + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, + [SORT_CRITERIA_S1]: sortValue1, + } as Record); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + JSON.stringify({ + [FILTER_CRITERIA_F3]: filterValue3, + [FILTER_CRITERIA_F4]: filterValue4, + }), + ]); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + SORT_KEY, + JSON.stringify({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), + [SORT_CRITERIA_S1]: sortValue1, + }), + ]); + + expect(observerSpy1).toHaveBeenCalledTimes(1); + expect(observerSpy1).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F4, filterValue4, "filter"], + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ]); + + expect(observerSpy2).not.toHaveBeenCalled(); + + expect(observerSpy3).toHaveBeenCalledTimes(1); + expect(observerSpy3).toHaveBeenCalledWith([ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F4, filterValue4, "filter"], + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `FiltersManager: Failed to notify mutation observers`, + observerError, + ); - describe('|bulkUpdate|', () => { - it(`should verify if value is Nil won't execute anything`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v'; - const filterValue2 = 'f2v'; - const sortValue1 = 's1v'; - const sortValue2 = 's2v'; - const mutationObserver = new MutationObserver(); - const observerSpy = spyOn(mutationObserver, 'observer').and.callThrough(); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F2] = filterValue2; - - manager.sortCriteria[SORT_CRITERIA_S1] = sortValue1; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - - manager.registerMutationObserver(mutationObserver.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: sortValue2 - } as Record); - - // When - manager.bulkUpdate(null); - - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F2]: filterValue2 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S1]: sortValue1, - [SORT_CRITERIA_S2]: sortValue2 - } as Record); - - expect(urlStateManagerStub.setQueryParam).not.toHaveBeenCalled(); - - expect(observerSpy).not.toHaveBeenCalled(); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will update values from Array of key-value (criteria-value) tuples, filter and sort`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v,f11v'; - const filterValue3 = 'f3v'; - const filterValue4 = 'f4v,f44v,f444v'; - const sortValue1 = 's1v'; - const sortValue2 = '-1'; - const sortValue3 = 's3v'; - const sortValue4 = 's4v'; - - const observerError = new Error('observer throws error'); - - const mutationObserver1 = new MutationObserver(); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.callThrough(); - const mutationObserver2 = new MutationObserver(); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const mutationObserver3 = new MutationObserver(); - const observerSpy3 = spyOn(mutationObserver3, 'observer').and.throwError(observerError); - - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; - manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; - - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - manager.registerMutationObserver(mutationObserver3.observer); - - manager.deleteMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: filterValue3 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4 - } as Record); - - // When - manager.bulkUpdate([ - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F3, filterValue3, 'filter'], - [SORT_CRITERIA_S3, null, 'sort'], - [FILTER_CRITERIA_F2, null, 'filter'], - ['someSort', '-1', 'sort'] as unknown as KeyValueTuple, - [FILTER_CRITERIA_F4, filterValue4, 'filter'], - ['someFilter', 'testValue', 'filter'] as unknown as KeyValueTuple, - [SORT_CRITERIA_S2, sortValue2, 'sort'], - [SORT_CRITERIA_S4, '', 'sort'] - ]); - - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F3]: filterValue3, - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, - [SORT_CRITERIA_S1]: sortValue1 - } as Record); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F3]: filterValue3, [FILTER_CRITERIA_F4]: filterValue4 }) - ]); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), [SORT_CRITERIA_S1]: sortValue1 }) - ]); - - expect(observerSpy1).toHaveBeenCalledTimes(1); - expect(observerSpy1).toHaveBeenCalledWith([ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F4, filterValue4, 'filter'], - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ]); - - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(observerSpy3).toHaveBeenCalledTimes(1); - expect(observerSpy3).toHaveBeenCalledWith([ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F4, filterValue4, 'filter'], - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith(`FiltersManager: Failed to notify mutation observers`, observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will update values from Array of key-value (criteria-value) tuples, filter and sort and clear previous values`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v,f11v'; - const filterValue3 = 'f3v'; - const filterValue4 = 'f4v,f44v,f444v'; - const sortValue1 = 's1v'; - const sortValue2 = '-1'; - const sortValue3 = 's3v'; - const sortValue4 = 's4v'; - - const observerError = new Error('observer throws error'); - - const mutationObserver1 = new MutationObserver(); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.callThrough(); - const mutationObserver2 = new MutationObserver(); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const mutationObserver3 = new MutationObserver(); - const observerSpy3 = spyOn(mutationObserver3, 'observer').and.throwError(observerError); - - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; - manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; - - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - manager.registerMutationObserver(mutationObserver3.observer); - - manager.deleteMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: filterValue3 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4 - } as Record); - - // When - manager.bulkUpdate( - [ - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [FILTER_CRITERIA_F3, filterValue3, 'filter'], - [FILTER_CRITERIA_F2, null, 'filter'], - ['someSort', '-1', 'sort'] as unknown as KeyValueTuple, - [FILTER_CRITERIA_F4, filterValue4, 'filter'], - ['someFilter', 'testValue', 'filter'] as unknown as KeyValueTuple, - [SORT_CRITERIA_S2, sortValue2, 'sort'], - [SORT_CRITERIA_S4, '', 'sort'] - ], - true - ); - - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F3]: filterValue3, - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, - [SORT_CRITERIA_S1]: sortValue1 - } as Record); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F3]: filterValue3, [FILTER_CRITERIA_F4]: filterValue4 }) - ]); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), [SORT_CRITERIA_S1]: sortValue1 }) - ]); - - expect(observerSpy1).toHaveBeenCalledTimes(1); - expect(observerSpy1).toHaveBeenCalledWith([ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F4, filterValue4, 'filter'], - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ]); - - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(observerSpy3).toHaveBeenCalledTimes(1); - expect(observerSpy3).toHaveBeenCalledWith([ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F4, filterValue4, 'filter'], - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith(`FiltersManager: Failed to notify mutation observers`, observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will update values from object of key-value (criteria-value) properties, filter and sort`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v,f11v'; - const filterValue3 = 'f3v'; - const sortValue1 = 's1v'; - const sortValue2 = '-1'; - const sortValue3 = 's3v'; - const sortValue4 = 's4v'; - - const observerError = new Error('observer throws error'); - - const mutationObserver1 = new MutationObserver(); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - const mutationObserver2 = new MutationObserver(); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const mutationObserver3 = new MutationObserver(); - const observerSpy3 = spyOn(mutationObserver3, 'observer').and.callThrough(); - - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; - manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; - - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - manager.registerMutationObserver(mutationObserver3.observer); - - manager.deleteMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: filterValue3 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4 - } as Record); - - // When - manager.bulkUpdate({ - [FILTER_KEY]: JSON.stringify({ - [FILTER_CRITERIA_F3]: `${filterValue3}10`, - [FILTER_CRITERIA_F4]: undefined, - someFilter: 'testValue' - }), - [SORT_KEY]: JSON.stringify({ - [SORT_CRITERIA_S1]: sortValue1, - someSort: -1, - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S4]: undefined - }) - }); - - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: `${filterValue3}10` - } as Record); - - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4, - [SORT_CRITERIA_S1]: sortValue1 - } as Record); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - FILTER_KEY, - JSON.stringify({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: `${filterValue3}10` - }) - ]); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ - SORT_KEY, - JSON.stringify({ - [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4, - [SORT_CRITERIA_S1]: sortValue1 - }) - ]); - - expect(observerSpy1).toHaveBeenCalledTimes(2); - expect(observerSpy1.calls.argsFor(0)).toEqual([[[FILTER_CRITERIA_F3, `${filterValue3}10`, 'filter']]]); - expect(observerSpy1.calls.argsFor(1)).toEqual([ - [ - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'] - ] - ]); - - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(observerSpy3).toHaveBeenCalledTimes(2); - expect(observerSpy3.calls.argsFor(0)).toEqual([[[FILTER_CRITERIA_F3, `${filterValue3}10`, 'filter']]]); - expect(observerSpy3.calls.argsFor(1)).toEqual([ - [ - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'] - ] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith(`FiltersManager: Failed to notify mutation observers`, observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will update values from object of key-value (criteria-value) properties, filter and sort and clear previous values`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v,f11v'; - const filterValue3 = 'f3v'; - const filterValue4 = 'f4v,f44v,f444v'; - const sortValue1 = 's1v'; - const sortValue2 = '-1'; - const sortValue3 = 's3v'; - const sortValue4 = 's4v'; - - const observerError = new Error('observer throws error'); - - const mutationObserver1 = new MutationObserver(); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - const mutationObserver2 = new MutationObserver(); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const mutationObserver3 = new MutationObserver(); - const observerSpy3 = spyOn(mutationObserver3, 'observer').and.callThrough(); - - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; - manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; - - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - manager.registerMutationObserver(mutationObserver3.observer); - - manager.deleteMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: filterValue3 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4 - } as Record); - - // When - manager.bulkUpdate( - { - [FILTER_KEY]: JSON.stringify({ - [FILTER_CRITERIA_F3]: `${filterValue3}10`, - [FILTER_CRITERIA_F4]: filterValue4, - someFilter: 'testValue' - }), - [SORT_KEY]: JSON.stringify({ - [SORT_CRITERIA_S1]: sortValue1, - someSort: -1, - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S4]: undefined - }) - }, - true - ); - - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F3]: `${filterValue3}10`, - [FILTER_CRITERIA_F4]: filterValue4 - } as Record); - - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, - [SORT_CRITERIA_S1]: sortValue1 - } as Record); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ - FILTER_KEY, - JSON.stringify({ [FILTER_CRITERIA_F3]: `${filterValue3}10`, [FILTER_CRITERIA_F4]: filterValue4 }) - ]); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ - SORT_KEY, - JSON.stringify({ [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), [SORT_CRITERIA_S1]: sortValue1 }) - ]); - - expect(observerSpy1).toHaveBeenCalledTimes(2); - expect(observerSpy1.calls.argsFor(0)).toEqual([ - [ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F3, `${filterValue3}10`, 'filter'], - [FILTER_CRITERIA_F4, filterValue4, 'filter'] - ] - ]); - expect(observerSpy1.calls.argsFor(1)).toEqual([ - [ - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ] - ]); - - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(observerSpy3).toHaveBeenCalledTimes(2); - expect(observerSpy3.calls.argsFor(0)).toEqual([ - [ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F3, `${filterValue3}10`, 'filter'], - [FILTER_CRITERIA_F4, filterValue4, 'filter'] - ] - ]); - expect(observerSpy3.calls.argsFor(1)).toEqual([ - [ - [SORT_CRITERIA_S1, sortValue1, 'sort'], - [SORT_CRITERIA_S2, parseInt(sortValue2, 10) as unknown as string, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith(`FiltersManager: Failed to notify mutation observers`, observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); - - it(`should verify will update values from object of key-value (criteria-value) properties, filter and sort are missing and clear previous values`, fakeAsync(() => { - // Given - const filterValue1 = 'f1v,f11v'; - const filterValue3 = 'f3v'; - const sortValue2 = '-1'; - const sortValue3 = 's3v'; - const sortValue4 = 's4v'; - - const observerError = new Error('observer throws error'); - - const mutationObserver1 = new MutationObserver(); - const observerSpy1 = spyOn(mutationObserver1, 'observer').and.throwError(observerError); - const mutationObserver2 = new MutationObserver(); - const observerSpy2 = spyOn(mutationObserver2, 'observer').and.callThrough(); - const mutationObserver3 = new MutationObserver(); - const observerSpy3 = spyOn(mutationObserver3, 'observer').and.callThrough(); - - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // prerequisites manager should have - manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; - manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; - manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; - manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; - manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; - - manager.registerMutationObserver(mutationObserver1.observer); - manager.registerMutationObserver(mutationObserver2.observer); - manager.registerMutationObserver(mutationObserver3.observer); - - manager.deleteMutationObserver(mutationObserver2.observer); - - // Then 1 - expect(manager.filterCriteria).toEqual({ - [FILTER_CRITERIA_F1]: filterValue1, - [FILTER_CRITERIA_F3]: filterValue3 - } as Record); - expect(manager.sortCriteria).toEqual({ - [SORT_CRITERIA_S2]: sortValue2, - [SORT_CRITERIA_S3]: sortValue3, - [SORT_CRITERIA_S4]: sortValue4 - } as Record); - - // When - manager.bulkUpdate({} as Record, true); - - tick(1000); - - // Then 2 - expect(manager.filterCriteria).toEqual({} as Record); - - expect(manager.sortCriteria).toEqual({} as Record); - - expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([FILTER_KEY, null]); - expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([SORT_KEY, null]); - - expect(observerSpy1).toHaveBeenCalledTimes(2); - expect(observerSpy1.calls.argsFor(0)).toEqual([ - [ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F3, null, 'filter'] - ] - ]); - expect(observerSpy1.calls.argsFor(1)).toEqual([ - [ - [SORT_CRITERIA_S2, null, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ] - ]); - - expect(observerSpy2).not.toHaveBeenCalled(); - - expect(observerSpy3).toHaveBeenCalledTimes(2); - expect(observerSpy3.calls.argsFor(0)).toEqual([ - [ - [FILTER_CRITERIA_F1, null, 'filter'], - [FILTER_CRITERIA_F3, null, 'filter'] - ] - ]); - expect(observerSpy3.calls.argsFor(1)).toEqual([ - [ - [SORT_CRITERIA_S2, null, 'sort'], - [SORT_CRITERIA_S3, null, 'sort'], - [SORT_CRITERIA_S4, null, 'sort'] - ] - ]); - - expect(consoleErrorSpy).toHaveBeenCalledWith(`FiltersManager: Failed to notify mutation observers`, observerError); - - expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); - expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); - expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); - })); + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will update values from object of key-value (criteria-value) properties, filter and sort`, fakeAsync(() => { + // Given + const filterValue1 = "f1v,f11v"; + const filterValue3 = "f3v"; + const sortValue1 = "s1v"; + const sortValue2 = "-1"; + const sortValue3 = "s3v"; + const sortValue4 = "s4v"; + + const observerError = new Error("observer throws error"); + + const mutationObserver1 = new MutationObserver(); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.throwError(observerError); + const mutationObserver2 = new MutationObserver(); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const mutationObserver3 = new MutationObserver(); + const observerSpy3 = spyOn( + mutationObserver3, + "observer", + ).and.callThrough(); + + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; + manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; + + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + manager.registerMutationObserver(mutationObserver3.observer); + + manager.deleteMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: filterValue3, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + } as Record); + + // When + manager.bulkUpdate({ + [FILTER_KEY]: JSON.stringify({ + [FILTER_CRITERIA_F3]: `${filterValue3}10`, + [FILTER_CRITERIA_F4]: undefined, + someFilter: "testValue", + }), + [SORT_KEY]: JSON.stringify({ + [SORT_CRITERIA_S1]: sortValue1, + someSort: -1, + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S4]: undefined, + }), }); + + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: `${filterValue3}10`, + } as Record); + + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + [SORT_CRITERIA_S1]: sortValue1, + } as Record); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + JSON.stringify({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: `${filterValue3}10`, + }), + ]); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + SORT_KEY, + JSON.stringify({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + [SORT_CRITERIA_S1]: sortValue1, + }), + ]); + + expect(observerSpy1).toHaveBeenCalledTimes(2); + expect(observerSpy1.calls.argsFor(0)).toEqual([ + [[FILTER_CRITERIA_F3, `${filterValue3}10`, "filter"]], + ]); + expect(observerSpy1.calls.argsFor(1)).toEqual([ + [ + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + ], + ]); + + expect(observerSpy2).not.toHaveBeenCalled(); + + expect(observerSpy3).toHaveBeenCalledTimes(2); + expect(observerSpy3.calls.argsFor(0)).toEqual([ + [[FILTER_CRITERIA_F3, `${filterValue3}10`, "filter"]], + ]); + expect(observerSpy3.calls.argsFor(1)).toEqual([ + [ + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + ], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `FiltersManager: Failed to notify mutation observers`, + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will update values from object of key-value (criteria-value) properties, filter and sort and clear previous values`, fakeAsync(() => { + // Given + const filterValue1 = "f1v,f11v"; + const filterValue3 = "f3v"; + const filterValue4 = "f4v,f44v,f444v"; + const sortValue1 = "s1v"; + const sortValue2 = "-1"; + const sortValue3 = "s3v"; + const sortValue4 = "s4v"; + + const observerError = new Error("observer throws error"); + + const mutationObserver1 = new MutationObserver(); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.throwError(observerError); + const mutationObserver2 = new MutationObserver(); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const mutationObserver3 = new MutationObserver(); + const observerSpy3 = spyOn( + mutationObserver3, + "observer", + ).and.callThrough(); + + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; + manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; + + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + manager.registerMutationObserver(mutationObserver3.observer); + + manager.deleteMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: filterValue3, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + } as Record); + + // When + manager.bulkUpdate( + { + [FILTER_KEY]: JSON.stringify({ + [FILTER_CRITERIA_F3]: `${filterValue3}10`, + [FILTER_CRITERIA_F4]: filterValue4, + someFilter: "testValue", + }), + [SORT_KEY]: JSON.stringify({ + [SORT_CRITERIA_S1]: sortValue1, + someSort: -1, + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S4]: undefined, + }), + }, + true, + ); + + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F3]: `${filterValue3}10`, + [FILTER_CRITERIA_F4]: filterValue4, + } as Record); + + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10) as unknown, + [SORT_CRITERIA_S1]: sortValue1, + } as Record); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + JSON.stringify({ + [FILTER_CRITERIA_F3]: `${filterValue3}10`, + [FILTER_CRITERIA_F4]: filterValue4, + }), + ]); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + SORT_KEY, + JSON.stringify({ + [SORT_CRITERIA_S2]: parseInt(sortValue2, 10), + [SORT_CRITERIA_S1]: sortValue1, + }), + ]); + + expect(observerSpy1).toHaveBeenCalledTimes(2); + expect(observerSpy1.calls.argsFor(0)).toEqual([ + [ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F3, `${filterValue3}10`, "filter"], + [FILTER_CRITERIA_F4, filterValue4, "filter"], + ], + ]); + expect(observerSpy1.calls.argsFor(1)).toEqual([ + [ + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ], + ]); + + expect(observerSpy2).not.toHaveBeenCalled(); + + expect(observerSpy3).toHaveBeenCalledTimes(2); + expect(observerSpy3.calls.argsFor(0)).toEqual([ + [ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F3, `${filterValue3}10`, "filter"], + [FILTER_CRITERIA_F4, filterValue4, "filter"], + ], + ]); + expect(observerSpy3.calls.argsFor(1)).toEqual([ + [ + [SORT_CRITERIA_S1, sortValue1, "sort"], + [ + SORT_CRITERIA_S2, + parseInt(sortValue2, 10) as unknown as string, + "sort", + ], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `FiltersManager: Failed to notify mutation observers`, + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); + + it(`should verify will update values from object of key-value (criteria-value) properties, filter and sort are missing and clear previous values`, fakeAsync(() => { + // Given + const filterValue1 = "f1v,f11v"; + const filterValue3 = "f3v"; + const sortValue2 = "-1"; + const sortValue3 = "s3v"; + const sortValue4 = "s4v"; + + const observerError = new Error("observer throws error"); + + const mutationObserver1 = new MutationObserver(); + const observerSpy1 = spyOn( + mutationObserver1, + "observer", + ).and.throwError(observerError); + const mutationObserver2 = new MutationObserver(); + const observerSpy2 = spyOn( + mutationObserver2, + "observer", + ).and.callThrough(); + const mutationObserver3 = new MutationObserver(); + const observerSpy3 = spyOn( + mutationObserver3, + "observer", + ).and.callThrough(); + + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // prerequisites manager should have + manager.filterCriteria[FILTER_CRITERIA_F1] = filterValue1; + manager.filterCriteria[FILTER_CRITERIA_F3] = filterValue3; + manager.sortCriteria[SORT_CRITERIA_S2] = sortValue2; + manager.sortCriteria[SORT_CRITERIA_S3] = sortValue3; + manager.sortCriteria[SORT_CRITERIA_S4] = sortValue4; + + manager.registerMutationObserver(mutationObserver1.observer); + manager.registerMutationObserver(mutationObserver2.observer); + manager.registerMutationObserver(mutationObserver3.observer); + + manager.deleteMutationObserver(mutationObserver2.observer); + + // Then 1 + expect(manager.filterCriteria).toEqual({ + [FILTER_CRITERIA_F1]: filterValue1, + [FILTER_CRITERIA_F3]: filterValue3, + } as Record); + expect(manager.sortCriteria).toEqual({ + [SORT_CRITERIA_S2]: sortValue2, + [SORT_CRITERIA_S3]: sortValue3, + [SORT_CRITERIA_S4]: sortValue4, + } as Record); + + // When + manager.bulkUpdate( + {} as Record, + true, + ); + + tick(1000); + + // Then 2 + expect(manager.filterCriteria).toEqual( + {} as Record, + ); + + expect(manager.sortCriteria).toEqual( + {} as Record, + ); + + expect(urlStateManagerStub.setQueryParam.calls.argsFor(0)).toEqual([ + FILTER_KEY, + null, + ]); + expect(urlStateManagerStub.setQueryParam.calls.argsFor(1)).toEqual([ + SORT_KEY, + null, + ]); + + expect(observerSpy1).toHaveBeenCalledTimes(2); + expect(observerSpy1.calls.argsFor(0)).toEqual([ + [ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F3, null, "filter"], + ], + ]); + expect(observerSpy1.calls.argsFor(1)).toEqual([ + [ + [SORT_CRITERIA_S2, null, "sort"], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ], + ]); + + expect(observerSpy2).not.toHaveBeenCalled(); + + expect(observerSpy3).toHaveBeenCalledTimes(2); + expect(observerSpy3.calls.argsFor(0)).toEqual([ + [ + [FILTER_CRITERIA_F1, null, "filter"], + [FILTER_CRITERIA_F3, null, "filter"], + ], + ]); + expect(observerSpy3.calls.argsFor(1)).toEqual([ + [ + [SORT_CRITERIA_S2, null, "sort"], + [SORT_CRITERIA_S3, null, "sort"], + [SORT_CRITERIA_S4, null, "sort"], + ], + ]); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `FiltersManager: Failed to notify mutation observers`, + observerError, + ); + + expect(urlStateManagerStub.locationToURL).not.toHaveBeenCalled(); + expect(urlStateManagerStub.replaceToUrl).not.toHaveBeenCalled(); + expect(urlStateManagerStub.navigateToUrl).not.toHaveBeenCalled(); + })); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts index cc46f1d8dc..f126993c74 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil, URLStateManager } from '@versatiledatakit/shared'; +import { CollectionsUtil, URLStateManager } from "@versatiledatakit/shared"; -export const SORT_KEY = 'sort'; -export const FILTER_KEY = 'filter'; +export const SORT_KEY = "sort"; +export const FILTER_KEY = "filter"; /** * ** Generic key-value (criteria-value) tuple in context of Filters and Sort @@ -14,21 +14,20 @@ export const FILTER_KEY = 'filter'; * * - 3rd value is very useful if filter and sort criteria overlaps by their names. */ -export type KeyValueTuple = [ - key: K, - value: V, - type?: typeof FILTER_KEY | typeof SORT_KEY -]; +export type KeyValueTuple< + K extends string | number, + V extends string | number, +> = [key: K, value: V, type?: typeof FILTER_KEY | typeof SORT_KEY]; /** * ** Mutation observer that could be registered in {@link FiltersSortManager} * * - Executed whenever mutation is registered in {@link FiltersSortManager} according its algorithm. */ export type FilterSortMutationObserver< - FC extends string, - FV extends string | number, - SC extends string = string, - SV extends string | number = string | number + FC extends string, + FV extends string | number, + SC extends string = string, + SV extends string | number = string | number, > = (changes: KeyValueTuple[]) => void; /** @@ -37,562 +36,655 @@ export type FilterSortMutationObserver< * - Leverages {@link URLStateManager} functionalities for Browser URL manipulation. */ export class FiltersSortManager< - FC extends string, - FV extends string | number, - SC extends string = string, - SV extends string | number = string | number + FC extends string, + FV extends string | number, + SC extends string = string, + SV extends string | number = string | number, > { - /** - * ** Filter criteria and value storage. - */ - readonly filterCriteria: Record = {} as Record; - /** - * ** Sort criteria and value storage. - */ - readonly sortCriteria: Record = {} as Record; - - /** - * ** Update strategy used in {@link URLStateManager}. - * @private - */ - private _updateStrategy: 'navigateToUrl' | 'locationToURL' | 'replaceToURL' = 'locationToURL'; - /** - * ** Debouncing used whenever Browser URL should be updated. - * @private - */ - private _debouncingTime = 300; // value is in milliseconds - /** - * ** Reference to scheduled timeout in conjunction with {@link _debouncingTime} - * @private - */ - private _updateTimeoutRef: number; - - /** - * ** Mutation observers storage. - * @private - */ - private readonly _mutationObservers: Set> = new Set< - FilterSortMutationObserver - >(); - - /** - * ** Constructor. - */ - constructor( - private readonly urlStateManager: URLStateManager, - private readonly knownFilterCriteria: FC[], - private readonly knownSortCriteria: SC[] - ) {} - - /** - * ** Returns true if requested criteria is found and its value is defined in Manager filter storage {@link filterCriteria}. - */ - hasFilter(criteria: FC): boolean { - return this.filterCriteria.hasOwnProperty(criteria) && CollectionsUtil.isDefined(this.filterCriteria[criteria]); + /** + * ** Filter criteria and value storage. + */ + readonly filterCriteria: Record = {} as Record; + /** + * ** Sort criteria and value storage. + */ + readonly sortCriteria: Record = {} as Record; + + /** + * ** Update strategy used in {@link URLStateManager}. + * @private + */ + private _updateStrategy: "navigateToUrl" | "locationToURL" | "replaceToURL" = + "locationToURL"; + /** + * ** Debouncing used whenever Browser URL should be updated. + * @private + */ + private _debouncingTime = 300; // value is in milliseconds + /** + * ** Reference to scheduled timeout in conjunction with {@link _debouncingTime} + * @private + */ + private _updateTimeoutRef: number; + + /** + * ** Mutation observers storage. + * @private + */ + private readonly _mutationObservers: Set< + FilterSortMutationObserver + > = new Set>(); + + /** + * ** Constructor. + */ + constructor( + private readonly urlStateManager: URLStateManager, + private readonly knownFilterCriteria: FC[], + private readonly knownSortCriteria: SC[], + ) {} + + /** + * ** Returns true if requested criteria is found and its value is defined in Manager filter storage {@link filterCriteria}. + */ + hasFilter(criteria: FC): boolean { + return ( + this.filterCriteria.hasOwnProperty(criteria) && + CollectionsUtil.isDefined(this.filterCriteria[criteria]) + ); + } + + /** + * ** Returns true if there is at least one filter with defined value in Manager filter storage {@link filterCriteria}. + */ + hasAnyFilter(): boolean { + return ( + CollectionsUtil.objectPairs(this.filterCriteria).filter(([_key, value]) => + CollectionsUtil.isDefined(value), + ).length > 0 + ); + } + + /** + * ** Set filter criteria in Manager filter storage {@link filterCriteria}. + * + * - on every filter set, Browser URL is updated with debouncing by default, and mutation observers are notified. + * - optionally: Browser URL update could be skipped if 3rd parameter is provided with false value. + */ + setFilter( + criteria: FC, + value: string | number, + updateBrowserUrl = true, + ): void { + if (this.filterCriteria[criteria] === value) { + return; } - /** - * ** Returns true if there is at least one filter with defined value in Manager filter storage {@link filterCriteria}. - */ - hasAnyFilter(): boolean { - return CollectionsUtil.objectPairs(this.filterCriteria).filter(([_key, value]) => CollectionsUtil.isDefined(value)).length > 0; + if (CollectionsUtil.isDefined(value)) { + this.filterCriteria[criteria] = `${value}` as FV; + } else { + delete this.filterCriteria[criteria]; } - /** - * ** Set filter criteria in Manager filter storage {@link filterCriteria}. - * - * - on every filter set, Browser URL is updated with debouncing by default, and mutation observers are notified. - * - optionally: Browser URL update could be skipped if 3rd parameter is provided with false value. - */ - setFilter(criteria: FC, value: string | number, updateBrowserUrl = true): void { - if (this.filterCriteria[criteria] === value) { - return; - } - - if (CollectionsUtil.isDefined(value)) { - this.filterCriteria[criteria] = `${value}` as FV; - } else { - delete this.filterCriteria[criteria]; - } - - this._serializeFilters(); + this._serializeFilters(); - if (updateBrowserUrl) { - this.updateBrowserUrl(); - } - - this._notifyMutationObservers([[criteria, value as FV, 'filter']]); + if (updateBrowserUrl) { + this.updateBrowserUrl(); } - /** - * ** Delete filter criteria from Manager filter storage {@link filterCriteria}. - * - * - on every delete filter, Browser URL is updated with debouncing by default, and mutation observers are notified. - * - optionally: Browser URL update could be skipped if 2nd parameter is provided with false value. - */ - deleteFilter(criteria: FC, updateBrowserUrl = true): boolean { - if (!this.hasFilter(criteria)) { - return false; - } + this._notifyMutationObservers([[criteria, value as FV, "filter"]]); + } + + /** + * ** Delete filter criteria from Manager filter storage {@link filterCriteria}. + * + * - on every delete filter, Browser URL is updated with debouncing by default, and mutation observers are notified. + * - optionally: Browser URL update could be skipped if 2nd parameter is provided with false value. + */ + deleteFilter(criteria: FC, updateBrowserUrl = true): boolean { + if (!this.hasFilter(criteria)) { + return false; + } - delete this.filterCriteria[criteria]; + delete this.filterCriteria[criteria]; - this._serializeFilters(); + this._serializeFilters(); - if (updateBrowserUrl) { - this.updateBrowserUrl(); - } + if (updateBrowserUrl) { + this.updateBrowserUrl(); + } - this._notifyMutationObservers([[criteria, null, 'filter']]); + this._notifyMutationObservers([[criteria, null, "filter"]]); - return true; - } + return true; + } - /** - * ** Clear all filters criteria from Manager filter storage {@link filterCriteria}. - * - * - on every clear filters, Browser URL is updated with debouncing by default, and mutation observers are notified. - * - optionally: Browser URL update could be skipped if 1st parameter is provided with false value. - * - optionally: Mutation observers notification could be skipped if 2nd parameter is provided with false value. - */ - clearFilters(updateBrowserUrl = true, notifyMutationObservers = true): void { - const filterPairs = CollectionsUtil.objectPairs(this.filterCriteria); - const mutatedFilterPairs: KeyValueTuple[] = filterPairs.map(([key]) => [key, null, 'filter'] as KeyValueTuple); - - filterPairs.forEach(([key]) => { - delete this.filterCriteria[key]; - }); + /** + * ** Clear all filters criteria from Manager filter storage {@link filterCriteria}. + * + * - on every clear filters, Browser URL is updated with debouncing by default, and mutation observers are notified. + * - optionally: Browser URL update could be skipped if 1st parameter is provided with false value. + * - optionally: Mutation observers notification could be skipped if 2nd parameter is provided with false value. + */ + clearFilters(updateBrowserUrl = true, notifyMutationObservers = true): void { + const filterPairs = CollectionsUtil.objectPairs(this.filterCriteria); + const mutatedFilterPairs: KeyValueTuple[] = filterPairs.map( + ([key]) => [key, null, "filter"] as KeyValueTuple, + ); - this._serializeFilters(); + filterPairs.forEach(([key]) => { + delete this.filterCriteria[key]; + }); - if (updateBrowserUrl) { - this.updateBrowserUrl(); - } + this._serializeFilters(); - if (notifyMutationObservers) { - this._notifyMutationObservers(mutatedFilterPairs); - } + if (updateBrowserUrl) { + this.updateBrowserUrl(); } - /** - * ** Returns true if requested criteria is found and its value is defined in Manager sort storage {@link sortCriteria}. - */ - hasSort(criteria: SC): boolean { - return this.sortCriteria.hasOwnProperty(criteria) && CollectionsUtil.isDefined(this.sortCriteria[criteria]); + if (notifyMutationObservers) { + this._notifyMutationObservers(mutatedFilterPairs); } - - /** - * ** Returns true if there is at least one sort with defined value in Manager sort storage {@link sortCriteria}. - */ - hasAnySort(): boolean { - return CollectionsUtil.objectPairs(this.sortCriteria).filter(([_key, value]) => CollectionsUtil.isDefined(value)).length > 0; + } + + /** + * ** Returns true if requested criteria is found and its value is defined in Manager sort storage {@link sortCriteria}. + */ + hasSort(criteria: SC): boolean { + return ( + this.sortCriteria.hasOwnProperty(criteria) && + CollectionsUtil.isDefined(this.sortCriteria[criteria]) + ); + } + + /** + * ** Returns true if there is at least one sort with defined value in Manager sort storage {@link sortCriteria}. + */ + hasAnySort(): boolean { + return ( + CollectionsUtil.objectPairs(this.sortCriteria).filter(([_key, value]) => + CollectionsUtil.isDefined(value), + ).length > 0 + ); + } + + /** + * ** Set sort criteria in Manager sort storage {@link sortCriteria}. + * + * - on every sort set, Browser URL is updated with debouncing by default, and mutation observers are notified. + * - optionally: Browser URL update could be skipped if 3rd parameter is provided with false value. + */ + setSort(criteria: SC, direction: SV, updateBrowserUrl = true): void { + if (this.sortCriteria[criteria] === direction) { + return; } - /** - * ** Set sort criteria in Manager sort storage {@link sortCriteria}. - * - * - on every sort set, Browser URL is updated with debouncing by default, and mutation observers are notified. - * - optionally: Browser URL update could be skipped if 3rd parameter is provided with false value. - */ - setSort(criteria: SC, direction: SV, updateBrowserUrl = true): void { - if (this.sortCriteria[criteria] === direction) { - return; - } - - if (CollectionsUtil.isDefined(direction)) { - this.sortCriteria[criteria] = direction; - } else { - delete this.sortCriteria[criteria]; - } - - this._serializeSort(); + if (CollectionsUtil.isDefined(direction)) { + this.sortCriteria[criteria] = direction; + } else { + delete this.sortCriteria[criteria]; + } - if (updateBrowserUrl) { - this.updateBrowserUrl(); - } + this._serializeSort(); - this._notifyMutationObservers([[criteria, direction, 'sort']]); + if (updateBrowserUrl) { + this.updateBrowserUrl(); } - /** - * ** Clear all sort criteria from Manager sort storage {@link sortCriteria}. - * - * - on every clear sort, Browser URL is updated with debouncing by default, and mutation observers are notified. - * - optionally: Browser URL update could be skipped if 1st parameter is provided with false value. - * - optionally: Mutation observers notification could be skipped if 2nd parameter is provided with false value. - */ - clearSort(updateBrowserUrl = true, notifyMutationObservers = true): void { - const mutatedSortPairs: KeyValueTuple[] = []; + this._notifyMutationObservers([[criteria, direction, "sort"]]); + } - CollectionsUtil.objectPairs(this.sortCriteria).forEach(([key]) => { - mutatedSortPairs.push([key, null, 'sort']); + /** + * ** Clear all sort criteria from Manager sort storage {@link sortCriteria}. + * + * - on every clear sort, Browser URL is updated with debouncing by default, and mutation observers are notified. + * - optionally: Browser URL update could be skipped if 1st parameter is provided with false value. + * - optionally: Mutation observers notification could be skipped if 2nd parameter is provided with false value. + */ + clearSort(updateBrowserUrl = true, notifyMutationObservers = true): void { + const mutatedSortPairs: KeyValueTuple[] = []; - delete this.sortCriteria[key]; - }); + CollectionsUtil.objectPairs(this.sortCriteria).forEach(([key]) => { + mutatedSortPairs.push([key, null, "sort"]); - this._serializeSort(); + delete this.sortCriteria[key]; + }); - if (updateBrowserUrl) { - this.updateBrowserUrl(); - } + this._serializeSort(); - if (notifyMutationObservers) { - this._notifyMutationObservers(mutatedSortPairs); - } + if (updateBrowserUrl) { + this.updateBrowserUrl(); } - /** - * ** Clear all filter and sort criteria from Manager storage {@link filterCriteria} {@link sortCriteria}. - * - * - on every clear, Browser URL is updated with debouncing by default, and mutation observers are notified. - * - optionally: Browser URL update could be skipped if 1st parameter is provided with false value. - * - optionally: Mutation observers notification could be skipped if 2nd parameter is provided with false value. - */ - clear(updateBrowserUrl = true, notifyMutationObservers = true): void { - this.clearFilters(updateBrowserUrl, notifyMutationObservers); - this.clearSort(updateBrowserUrl, notifyMutationObservers); + if (notifyMutationObservers) { + this._notifyMutationObservers(mutatedSortPairs); + } + } + + /** + * ** Clear all filter and sort criteria from Manager storage {@link filterCriteria} {@link sortCriteria}. + * + * - on every clear, Browser URL is updated with debouncing by default, and mutation observers are notified. + * - optionally: Browser URL update could be skipped if 1st parameter is provided with false value. + * - optionally: Mutation observers notification could be skipped if 2nd parameter is provided with false value. + */ + clear(updateBrowserUrl = true, notifyMutationObservers = true): void { + this.clearFilters(updateBrowserUrl, notifyMutationObservers); + this.clearSort(updateBrowserUrl, notifyMutationObservers); + } + + /** + * ** Bulk update Manager storages for filter and sort criteria using key-value (criteria-value) tuples + * with optional 3rd value that clarify the criteria type (either filter or sort). + */ + bulkUpdate( + filterSortPairs: KeyValueTuple[], + clearPreviousValues?: boolean, + ); + /** + * ** Bulk update Manager storages for filter and sort criteria leveraging provided nested object criteria-value. + */ + bulkUpdate( + filterValues: Record, + clearPreviousValues?: boolean, + ): void; + /** + * @inheritDoc + */ + bulkUpdate( + updates: + | Record + | KeyValueTuple[], + clearPreviousValues = false, + ): void { + // Nil (null or undefined) skipped execution + if (CollectionsUtil.isNil(updates)) { + return; } - /** - * ** Bulk update Manager storages for filter and sort criteria using key-value (criteria-value) tuples - * with optional 3rd value that clarify the criteria type (either filter or sort). - */ - bulkUpdate(filterSortPairs: KeyValueTuple[], clearPreviousValues?: boolean); - /** - * ** Bulk update Manager storages for filter and sort criteria leveraging provided nested object criteria-value. - */ - bulkUpdate(filterValues: Record, clearPreviousValues?: boolean): void; - /** - * @inheritDoc - */ - bulkUpdate( - updates: Record | KeyValueTuple[], - clearPreviousValues = false - ): void { - // Nil (null or undefined) skipped execution - if (CollectionsUtil.isNil(updates)) { - return; - } - - // Array means key-value tuples provided - if (CollectionsUtil.isArray(updates)) { - const mutatedFilterPairs: KeyValueTuple[] = this._persistBulkFilters( - updates as KeyValueTuple[], - clearPreviousValues - ); - this._serializeFilters(); - - const mutatedSortPairs: KeyValueTuple[] = this._persistBulkSort( - updates as KeyValueTuple[], - clearPreviousValues - ); - this._serializeSort(); - - this._notifyMutationObservers([...mutatedFilterPairs, ...mutatedSortPairs]); - - return; - } - - // otherwise presume it's and object with filter - if ((updates as object).hasOwnProperty(FILTER_KEY) && CollectionsUtil.isStringWithContent(updates[FILTER_KEY])) { - const deserializedFilterCriteria = this._deserializeFilters(updates[FILTER_KEY]); - const normalizedFilterCriteria: KeyValueTuple[] = CollectionsUtil.objectPairs(deserializedFilterCriteria).map( - (filterPairs) => [filterPairs[0], filterPairs[1], 'filter'] as unknown as KeyValueTuple - ); - - const mutatedFilterPairs: KeyValueTuple[] = this._persistBulkFilters(normalizedFilterCriteria, clearPreviousValues); - this._serializeFilters(); - - this._notifyMutationObservers(mutatedFilterPairs); - } else if (clearPreviousValues) { - this.clearFilters(false); - } - - // otherwise presume it's and object with sort - if ((updates as object).hasOwnProperty(SORT_KEY) && CollectionsUtil.isStringWithContent(updates[SORT_KEY])) { - const deserializedSortCriteria = this._deserializeSort(updates[SORT_KEY]); - const normalizedSortCriteria: KeyValueTuple[] = CollectionsUtil.objectPairs(deserializedSortCriteria).map( - (sortPairs) => [sortPairs[0], sortPairs[1], 'sort'] as unknown as KeyValueTuple - ); - - const mutatedSortPairs: KeyValueTuple[] = this._persistBulkSort(normalizedSortCriteria, clearPreviousValues); - this._serializeSort(); - - this._notifyMutationObservers(mutatedSortPairs); - } else if (clearPreviousValues) { - this.clearSort(false); - } + // Array means key-value tuples provided + if (CollectionsUtil.isArray(updates)) { + const mutatedFilterPairs: KeyValueTuple[] = + this._persistBulkFilters( + updates as KeyValueTuple[], + clearPreviousValues, + ); + this._serializeFilters(); + + const mutatedSortPairs: KeyValueTuple[] = this._persistBulkSort( + updates as KeyValueTuple[], + clearPreviousValues, + ); + this._serializeSort(); + + this._notifyMutationObservers([ + ...mutatedFilterPairs, + ...mutatedSortPairs, + ]); + + return; } - /** - * ** Update Browser URL either with predefined strategy or with one time update strategy provided as parameter. - * - * - Updates are debounced by default. - * - optionally: debounce could be skipped and Browser URL would be updated immediately if 2nd parameter is provided with false value. - */ - updateBrowserUrl(updateStrategy?: 'navigateToUrl' | 'locationToURL' | 'replaceToURL', skipDebouncing = false): void { - const strategy = CollectionsUtil.isStringWithContent(updateStrategy) ? updateStrategy : this._updateStrategy; + // otherwise presume it's and object with filter + if ( + (updates as object).hasOwnProperty(FILTER_KEY) && + CollectionsUtil.isStringWithContent(updates[FILTER_KEY]) + ) { + const deserializedFilterCriteria = this._deserializeFilters( + updates[FILTER_KEY], + ); + const normalizedFilterCriteria: KeyValueTuple[] = + CollectionsUtil.objectPairs(deserializedFilterCriteria).map( + (filterPairs) => + [ + filterPairs[0], + filterPairs[1], + "filter", + ] as unknown as KeyValueTuple, + ); + + const mutatedFilterPairs: KeyValueTuple[] = + this._persistBulkFilters(normalizedFilterCriteria, clearPreviousValues); + this._serializeFilters(); + + this._notifyMutationObservers(mutatedFilterPairs); + } else if (clearPreviousValues) { + this.clearFilters(false); + } - this.cancelScheduledBrowserUrlUpdate(); + // otherwise presume it's and object with sort + if ( + (updates as object).hasOwnProperty(SORT_KEY) && + CollectionsUtil.isStringWithContent(updates[SORT_KEY]) + ) { + const deserializedSortCriteria = this._deserializeSort(updates[SORT_KEY]); + const normalizedSortCriteria: KeyValueTuple[] = + CollectionsUtil.objectPairs(deserializedSortCriteria).map( + (sortPairs) => + [sortPairs[0], sortPairs[1], "sort"] as unknown as KeyValueTuple< + SC, + SV + >, + ); + + const mutatedSortPairs: KeyValueTuple[] = this._persistBulkSort( + normalizedSortCriteria, + clearPreviousValues, + ); + this._serializeSort(); + + this._notifyMutationObservers(mutatedSortPairs); + } else if (clearPreviousValues) { + this.clearSort(false); + } + } + + /** + * ** Update Browser URL either with predefined strategy or with one time update strategy provided as parameter. + * + * - Updates are debounced by default. + * - optionally: debounce could be skipped and Browser URL would be updated immediately if 2nd parameter is provided with false value. + */ + updateBrowserUrl( + updateStrategy?: "navigateToUrl" | "locationToURL" | "replaceToURL", + skipDebouncing = false, + ): void { + const strategy = CollectionsUtil.isStringWithContent(updateStrategy) + ? updateStrategy + : this._updateStrategy; + + this.cancelScheduledBrowserUrlUpdate(); + + if (skipDebouncing) { + this._doUpdateBrowserUrl(strategy); + + return; + } - if (skipDebouncing) { - this._doUpdateBrowserUrl(strategy); + // debouncing for update URL, to avoid multiple updates when there are multiple serial near close update events + this._updateTimeoutRef = setTimeout(() => { + this._doUpdateBrowserUrl(strategy); - return; - } + this._updateTimeoutRef = null; + }, this._debouncingTime); + } - // debouncing for update URL, to avoid multiple updates when there are multiple serial near close update events - this._updateTimeoutRef = setTimeout(() => { - this._doUpdateBrowserUrl(strategy); + /** + * ** Cancel scheduled (debounced) Browser URL update. + * + * - if canceled it won't update until next change occurs or {@link updateBrowserUrl} method is invoked on demand. + */ + cancelScheduledBrowserUrlUpdate(): void { + if (CollectionsUtil.isNumber(this._updateTimeoutRef)) { + clearTimeout(this._updateTimeoutRef); - this._updateTimeoutRef = null; - }, this._debouncingTime); + this._updateTimeoutRef = null; } - - /** - * ** Cancel scheduled (debounced) Browser URL update. - * - * - if canceled it won't update until next change occurs or {@link updateBrowserUrl} method is invoked on demand. - */ - cancelScheduledBrowserUrlUpdate(): void { - if (CollectionsUtil.isNumber(this._updateTimeoutRef)) { - clearTimeout(this._updateTimeoutRef); - - this._updateTimeoutRef = null; - } + } + + /** + * ** Change Manager Base url. + * + * - it will update {@link URLStateManager} base url. + */ + changeBaseUrl(baseUrl: string): void { + this.urlStateManager.changeBaseUrl(baseUrl); + } + + /** + * ** Change Manager default update strategy. + */ + changeUpdateStrategy( + strategy: "navigateToUrl" | "locationToURL" | "replaceToURL", + ): void { + this._updateStrategy = strategy; + } + + /** + * ** Change Manager default debouncing time. + */ + changeDebouncingTime(debouncingTime: number): void { + if ( + !CollectionsUtil.isNumber(debouncingTime) || + CollectionsUtil.isNaN(debouncingTime) + ) { + return; } - /** - * ** Change Manager Base url. - * - * - it will update {@link URLStateManager} base url. - */ - changeBaseUrl(baseUrl: string): void { - this.urlStateManager.changeBaseUrl(baseUrl); + this._debouncingTime = debouncingTime; + } + + /** + * ** Register mutation observer, that will be invoked whenever mutation occurs in manager, either filter or sort mutation. + */ + registerMutationObserver( + callback: FilterSortMutationObserver, + ): void { + if (this._mutationObservers.has(callback)) { + return; } - /** - * ** Change Manager default update strategy. - */ - changeUpdateStrategy(strategy: 'navigateToUrl' | 'locationToURL' | 'replaceToURL'): void { - this._updateStrategy = strategy; + this._mutationObservers.add(callback); + } + + /** + * ** Delete mutation observer. + */ + deleteMutationObserver( + callback: FilterSortMutationObserver, + ): boolean { + if (!this._mutationObservers.has(callback)) { + return false; } - /** - * ** Change Manager default debouncing time. - */ - changeDebouncingTime(debouncingTime: number): void { - if (!CollectionsUtil.isNumber(debouncingTime) || CollectionsUtil.isNaN(debouncingTime)) { - return; - } + this._mutationObservers.delete(callback); - this._debouncingTime = debouncingTime; - } + return true; + } - /** - * ** Register mutation observer, that will be invoked whenever mutation occurs in manager, either filter or sort mutation. - */ - registerMutationObserver(callback: FilterSortMutationObserver): void { - if (this._mutationObservers.has(callback)) { - return; - } + /** + * ** Persist filter tuple pairs of key-value in filter storage {@link filterCriteria}. + * @private + */ + private _persistBulkFilters( + filterPairs: KeyValueTuple[], + clearPreviousValues: boolean, + ): KeyValueTuple[] { + const mutatedFilterPairs: KeyValueTuple[] = []; - this._mutationObservers.add(callback); - } + for (const knownCriteria of this.knownFilterCriteria) { + const foundFilterPairs = filterPairs.filter( + ([criteria, _value, type]) => + criteria === knownCriteria && type === "filter", + ); - /** - * ** Delete mutation observer. - */ - deleteMutationObserver(callback: FilterSortMutationObserver): boolean { - if (!this._mutationObservers.has(callback)) { - return false; - } + if (foundFilterPairs.length > 0) { + const value = foundFilterPairs.pop()[1]; - this._mutationObservers.delete(callback); + if (CollectionsUtil.isDefined(value)) { + if (this.filterCriteria[knownCriteria] !== value) { + this.filterCriteria[knownCriteria] = value; - return true; - } + mutatedFilterPairs.push([knownCriteria, value, "filter"]); + } + } else if (this.hasFilter(knownCriteria)) { + delete this.filterCriteria[knownCriteria]; - /** - * ** Persist filter tuple pairs of key-value in filter storage {@link filterCriteria}. - * @private - */ - private _persistBulkFilters(filterPairs: KeyValueTuple[], clearPreviousValues: boolean): KeyValueTuple[] { - const mutatedFilterPairs: KeyValueTuple[] = []; + mutatedFilterPairs.push([knownCriteria, null, "filter"]); + } + } else if (clearPreviousValues) { + if (this.hasFilter(knownCriteria)) { + delete this.filterCriteria[knownCriteria]; - for (const knownCriteria of this.knownFilterCriteria) { - const foundFilterPairs = filterPairs.filter(([criteria, _value, type]) => criteria === knownCriteria && type === 'filter'); + mutatedFilterPairs.push([knownCriteria, null, "filter"]); + } + } + } - if (foundFilterPairs.length > 0) { - const value = foundFilterPairs.pop()[1]; + return mutatedFilterPairs; + } - if (CollectionsUtil.isDefined(value)) { - if (this.filterCriteria[knownCriteria] !== value) { - this.filterCriteria[knownCriteria] = value; + /** + * ** Persist sort tuple pairs of key-value in sort storage {@link sortCriteria}. + * @private + */ + private _persistBulkSort( + sortPairs: KeyValueTuple[], + clearPreviousValues: boolean, + ): KeyValueTuple[] { + const mutatedSortPairs: KeyValueTuple[] = []; - mutatedFilterPairs.push([knownCriteria, value, 'filter']); - } - } else if (this.hasFilter(knownCriteria)) { - delete this.filterCriteria[knownCriteria]; + for (const knownCriteria of this.knownSortCriteria) { + const foundSortPairs = sortPairs.filter( + ([criteria, _direction, type]) => + criteria === knownCriteria && type === "sort", + ); - mutatedFilterPairs.push([knownCriteria, null, 'filter']); - } - } else if (clearPreviousValues) { - if (this.hasFilter(knownCriteria)) { - delete this.filterCriteria[knownCriteria]; + if (foundSortPairs.length > 0) { + const value = foundSortPairs.pop()[1]; - mutatedFilterPairs.push([knownCriteria, null, 'filter']); - } + if (CollectionsUtil.isDefined(value)) { + let normalizedValue: SV; + + if (CollectionsUtil.isString(value)) { + if (/^(-)?\d+$/.test(value)) { + normalizedValue = parseInt(value, 10) as SV; + } else if (CollectionsUtil.isStringWithContent(value)) { + normalizedValue = value; + } else { + normalizedValue = null; } - } + } else { + normalizedValue = value; + } - return mutatedFilterPairs; - } + if (CollectionsUtil.isDefined(normalizedValue)) { + if (this.sortCriteria[knownCriteria] !== normalizedValue) { + this.sortCriteria[knownCriteria] = normalizedValue; - /** - * ** Persist sort tuple pairs of key-value in sort storage {@link sortCriteria}. - * @private - */ - private _persistBulkSort(sortPairs: KeyValueTuple[], clearPreviousValues: boolean): KeyValueTuple[] { - const mutatedSortPairs: KeyValueTuple[] = []; - - for (const knownCriteria of this.knownSortCriteria) { - const foundSortPairs = sortPairs.filter(([criteria, _direction, type]) => criteria === knownCriteria && type === 'sort'); - - if (foundSortPairs.length > 0) { - const value = foundSortPairs.pop()[1]; - - if (CollectionsUtil.isDefined(value)) { - let normalizedValue: SV; - - if (CollectionsUtil.isString(value)) { - if (/^(-)?\d+$/.test(value)) { - normalizedValue = parseInt(value, 10) as SV; - } else if (CollectionsUtil.isStringWithContent(value)) { - normalizedValue = value; - } else { - normalizedValue = null; - } - } else { - normalizedValue = value; - } - - if (CollectionsUtil.isDefined(normalizedValue)) { - if (this.sortCriteria[knownCriteria] !== normalizedValue) { - this.sortCriteria[knownCriteria] = normalizedValue; - - mutatedSortPairs.push([knownCriteria, normalizedValue, 'sort']); - } - } else if (this.hasSort(knownCriteria)) { - delete this.sortCriteria[knownCriteria]; - - mutatedSortPairs.push([knownCriteria, null, 'sort']); - } - } else if (this.hasSort(knownCriteria)) { - delete this.sortCriteria[knownCriteria]; - - mutatedSortPairs.push([knownCriteria, null, 'sort']); - } - } else if (clearPreviousValues) { - if (this.hasSort(knownCriteria)) { - delete this.sortCriteria[knownCriteria]; - - mutatedSortPairs.push([knownCriteria, null, 'sort']); - } + mutatedSortPairs.push([knownCriteria, normalizedValue, "sort"]); } - } - - return mutatedSortPairs; - } + } else if (this.hasSort(knownCriteria)) { + delete this.sortCriteria[knownCriteria]; - /** - * ** Serialize filters for query params. - * @private - */ - private _serializeFilters(): void { - const filterPairs = CollectionsUtil.objectPairs(this.filterCriteria); - const normalizedFilterPairs: string = filterPairs.length > 0 ? JSON.stringify(this.filterCriteria) : null; - - this._updateUrlStateManager([FILTER_KEY, normalizedFilterPairs]); - } + mutatedSortPairs.push([knownCriteria, null, "sort"]); + } + } else if (this.hasSort(knownCriteria)) { + delete this.sortCriteria[knownCriteria]; - private _deserializeFilters(value: string): Record { - try { - return JSON.parse(value) as Record; - } catch (error) { - console.error(`FiltersManager: Failed to parse Filters`, error); + mutatedSortPairs.push([knownCriteria, null, "sort"]); + } + } else if (clearPreviousValues) { + if (this.hasSort(knownCriteria)) { + delete this.sortCriteria[knownCriteria]; - return {} as Record; + mutatedSortPairs.push([knownCriteria, null, "sort"]); } + } } - /** - * ** Serialize sort for query params. - * @private - */ - private _serializeSort(): void { - const sortPairs = CollectionsUtil.objectPairs(this.sortCriteria); - const normalizedSortPairs: string = sortPairs.length > 0 ? JSON.stringify(this.sortCriteria) : null; + return mutatedSortPairs; + } - this._updateUrlStateManager([SORT_KEY, normalizedSortPairs]); - } + /** + * ** Serialize filters for query params. + * @private + */ + private _serializeFilters(): void { + const filterPairs = CollectionsUtil.objectPairs(this.filterCriteria); + const normalizedFilterPairs: string = + filterPairs.length > 0 ? JSON.stringify(this.filterCriteria) : null; - private _deserializeSort(value: string): Record { - try { - return JSON.parse(value) as Record; - } catch (error) { - console.error(`FiltersManager: Failed to parse Sort`, error); + this._updateUrlStateManager([FILTER_KEY, normalizedFilterPairs]); + } - return {} as Record; - } - } + private _deserializeFilters(value: string): Record { + try { + return JSON.parse(value) as Record; + } catch (error) { + console.error(`FiltersManager: Failed to parse Filters`, error); - /** - * ** Actual update for Browser URL through {@link URLStateManager}. - * @private - */ - private _doUpdateBrowserUrl(strategy: 'navigateToUrl' | 'locationToURL' | 'replaceToURL'): void { - if (strategy === 'locationToURL') { - this.urlStateManager.locationToURL(); - } else if (strategy === 'replaceToURL') { - this.urlStateManager.replaceToUrl(); - } else { - this.urlStateManager - .navigateToUrl() - .then(() => { - // No-op. - }) - .catch((error) => { - console.error(`FiltersManager: Failed to update Browser Url`, error); - }); - } + return {} as Record; } - - /** - * ** Update {@link URLStateManager} query params using provided key-value tuples. - * @private - */ - private _updateUrlStateManager(...updatePairs: KeyValueTuple[]): void { - for (const [criteria, value] of updatePairs) { - this.urlStateManager.setQueryParam(criteria, value); - } + } + + /** + * ** Serialize sort for query params. + * @private + */ + private _serializeSort(): void { + const sortPairs = CollectionsUtil.objectPairs(this.sortCriteria); + const normalizedSortPairs: string = + sortPairs.length > 0 ? JSON.stringify(this.sortCriteria) : null; + + this._updateUrlStateManager([SORT_KEY, normalizedSortPairs]); + } + + private _deserializeSort(value: string): Record { + try { + return JSON.parse(value) as Record; + } catch (error) { + console.error(`FiltersManager: Failed to parse Sort`, error); + + return {} as Record; } - - /** - * ** Notify mutation observers providing Array of tuples for mutated key-value. - * @private - */ - private _notifyMutationObservers(changes: KeyValueTuple[]): void { - if (changes.length === 0) { - return; - } - - this._mutationObservers.forEach((observer) => { - try { - observer(changes); - } catch (error) { - console.error(`FiltersManager: Failed to notify mutation observers`, error); - } + } + + /** + * ** Actual update for Browser URL through {@link URLStateManager}. + * @private + */ + private _doUpdateBrowserUrl( + strategy: "navigateToUrl" | "locationToURL" | "replaceToURL", + ): void { + if (strategy === "locationToURL") { + this.urlStateManager.locationToURL(); + } else if (strategy === "replaceToURL") { + this.urlStateManager.replaceToUrl(); + } else { + this.urlStateManager + .navigateToUrl() + .then(() => { + // No-op. + }) + .catch((error) => { + console.error(`FiltersManager: Failed to update Browser Url`, error); }); } + } + + /** + * ** Update {@link URLStateManager} query params using provided key-value tuples. + * @private + */ + private _updateUrlStateManager( + ...updatePairs: KeyValueTuple[] + ): void { + for (const [criteria, value] of updatePairs) { + this.urlStateManager.setQueryParam(criteria, value); + } + } + + /** + * ** Notify mutation observers providing Array of tuples for mutated key-value. + * @private + */ + private _notifyMutationObservers( + changes: KeyValueTuple[], + ): void { + if (changes.length === 0) { + return; + } + + this._mutationObservers.forEach((observer) => { + try { + observer(changes); + } catch (error) { + console.error( + `FiltersManager: Failed to notify mutation observers`, + error, + ); + } + }); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/index.ts index c6d1381800..d7725218b9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './filters-sort-manager'; +export * from "./filters-sort-manager"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/index.ts index 7d85451e59..cd63e4da35 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './filters-manager'; +export * from "./filters-manager"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/_data-jobs-base-grid.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/_data-jobs-base-grid.component.scss index db7d65e58c..02b6e64f61 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/_data-jobs-base-grid.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/_data-jobs-base-grid.component.scss @@ -4,116 +4,116 @@ */ :host { - .label-link { - cursor: pointer; + .label-link { + cursor: pointer; + } + + ::ng-deep { + lib-grid-action { + display: flex; + flex-direction: column; + + lib-quick-filters { + clr-icon { + margin-left: -5px; + margin-right: 3px; + margin-top: 1px; + + &.status-icon-enabled { + color: #5aa220; + } + + &.status-icon-disabled { + color: var(--clr-color-neutral-600); + } + } + } } - ::ng-deep { - lib-grid-action { - display: flex; - flex-direction: column; - - lib-quick-filters { - clr-icon { - margin-left: -5px; - margin-right: 3px; - margin-top: 1px; - - &.status-icon-enabled { - color: #5aa220; - } - - &.status-icon-disabled { - color: var(--clr-color-neutral-600); - } - } - } + clr-datagrid { + height: 100%; + + .datagrid-column, + .datagrid-cell { + &.column__min-width--xs { + min-width: 3.25rem; } - clr-datagrid { - height: 100%; + &.column__min-width--s { + min-width: 4.25rem; + } - .datagrid-column, - .datagrid-cell { - &.column__min-width--xs { - min-width: 3.25rem; - } - - &.column__min-width--s { - min-width: 4.25rem; - } - - &.column__min-width--m { - min-width: 4.8rem; - } - - &.column__min-width--l { - min-width: 5.5rem; - } - - &.column__min-width--xl { - min-width: 6.5rem; - } - - &.column__max-width--xs { - max-width: 5rem; - } - - &.column__max-width--s { - max-width: 6rem; - } - - &.column__max-width--m { - max-width: 7rem; - } - - &.column__max-width--l { - max-width: 8.5rem; - } - - &.column__max-width--xl { - max-width: 10rem; - } - - &.jobs-list__column-opener { - min-width: 75px; - width: 75px; - } - } - - .datagrid-outer-wrapper { - .datagrid-inner-wrapper { - .datagrid { - flex-basis: 0; - margin-top: 0; - } - - .datagrid-spinner { - top: 0; - height: 100%; - } - } - } + &.column__min-width--m { + min-width: 4.8rem; } - } - .grid-container { - height: 100%; - display: flex; - flex-direction: column; + &.column__min-width--l { + min-width: 5.5rem; + } - .container { + &.column__min-width--xl { + min-width: 6.5rem; + } + + &.column__max-width--xs { + max-width: 5rem; + } + + &.column__max-width--s { + max-width: 6rem; + } + + &.column__max-width--m { + max-width: 7rem; + } + + &.column__max-width--l { + max-width: 8.5rem; + } + + &.column__max-width--xl { + max-width: 10rem; + } + + &.jobs-list__column-opener { + min-width: 75px; + width: 75px; + } + } + + .datagrid-outer-wrapper { + .datagrid-inner-wrapper { + .datagrid { + flex-basis: 0; + margin-top: 0; + } + + .datagrid-spinner { + top: 0; height: 100%; + } } + } } + } - .grid-container-compact { - height: auto; - display: flex; - flex-direction: column; + .grid-container { + height: 100%; + display: flex; + flex-direction: column; - .container-compact { - height: 50vh; - } + .container { + height: 100%; + } + } + + .grid-container-compact { + height: auto; + display: flex; + flex-direction: column; + + .container-compact { + height: 50vh; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/data-jobs-base-grid.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/data-jobs-base-grid.component.ts index 7f12cbcec9..15a3e31774 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/data-jobs-base-grid.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/base-grid/data-jobs-base-grid.component.ts @@ -5,927 +5,1100 @@ /* eslint-disable @typescript-eslint/naming-convention,@angular-eslint/directive-class-suffix */ -import { Directive, ElementRef, Input, OnInit } from '@angular/core'; -import { Location } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Directive, ElementRef, Input, OnInit } from "@angular/core"; +import { Location } from "@angular/common"; +import { ActivatedRoute, Router } from "@angular/router"; -import { Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, take } from 'rxjs/operators'; +import { Subject } from "rxjs"; +import { debounceTime, distinctUntilChanged, take } from "rxjs/operators"; -import { ClrDatagridSortOrder, ClrDatagridStateInterface } from '@clr/angular'; +import { ClrDatagridSortOrder, ClrDatagridStateInterface } from "@clr/angular"; import { - ApiPredicate, - ASC, - CollectionsUtil, - ComponentModel, - ComponentService, - DESC, - ErrorHandlerService, - ErrorRecord, - NavigationService, - OnTaurusModelChange, - OnTaurusModelError, - OnTaurusModelInit, - OnTaurusModelInitialLoad, - OnTaurusModelLoad, - RouterService, - RouterState, - RouteState, - TaurusBaseComponent, - URLStateManager -} from '@versatiledatakit/shared'; - -import { ErrorUtil } from '../../shared/utils'; - -import { QuickFilters } from '../../shared/components'; + ApiPredicate, + ASC, + CollectionsUtil, + ComponentModel, + ComponentService, + DESC, + ErrorHandlerService, + ErrorRecord, + NavigationService, + OnTaurusModelChange, + OnTaurusModelError, + OnTaurusModelInit, + OnTaurusModelInitialLoad, + OnTaurusModelLoad, + RouterService, + RouterState, + RouteState, + TaurusBaseComponent, + URLStateManager, +} from "@versatiledatakit/shared"; + +import { ErrorUtil } from "../../shared/utils"; + +import { QuickFilters } from "../../shared/components"; import { - DataJob, - DataJobExecutionStatus, - DataJobStatus, - DataPipelinesConfig, - DataPipelinesRestoreUI, - DisplayMode, - GridFilters, - JOBS_DATA_KEY -} from '../../model'; + DataJob, + DataJobExecutionStatus, + DataJobStatus, + DataPipelinesConfig, + DataPipelinesRestoreUI, + DisplayMode, + GridFilters, + JOBS_DATA_KEY, +} from "../../model"; -import { TASK_LOAD_JOBS_STATE } from '../../state/tasks'; +import { TASK_LOAD_JOBS_STATE } from "../../state/tasks"; -import { LOAD_JOBS_ERROR_CODES } from '../../state/error-codes'; +import { LOAD_JOBS_ERROR_CODES } from "../../state/error-codes"; -import { DataJobsApiService, DataJobsService } from '../../services'; +import { DataJobsApiService, DataJobsService } from "../../services"; -export const QUERY_PARAM_SEARCH = 'search'; +export const QUERY_PARAM_SEARCH = "search"; export type ClrGridUIState = { - totalItems: number; - lastPage: number; - pageSize: number; - filter: GridFilters; - sort: { [key: string]: ClrDatagridSortOrder }; - search: string; + totalItems: number; + lastPage: number; + pageSize: number; + filter: GridFilters; + sort: { [key: string]: ClrDatagridSortOrder }; + search: string; }; export type UIElementOffset = { x: number; y: number }; export type DataJobsLocalStorageUserConfig = { - hiddenColumns: { [columnName: string]: boolean }; + hiddenColumns: { [columnName: string]: boolean }; }; @Directive() export abstract class DataJobsBaseGridComponent - extends TaurusBaseComponent - implements OnInit, OnTaurusModelInit, OnTaurusModelInitialLoad, OnTaurusModelLoad, OnTaurusModelChange, OnTaurusModelError + extends TaurusBaseComponent + implements + OnInit, + OnTaurusModelInit, + OnTaurusModelInitialLoad, + OnTaurusModelLoad, + OnTaurusModelChange, + OnTaurusModelError { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DataJobsBaseGridComponent'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'DataJobs-BaseGrid-Component'; - - static readonly UI_KEY_PAGE_OFFSET = 'pageOffset'; - static readonly UI_KEY_GRID_OFFSET = 'gridOffset'; - static readonly UI_KEY_GRID_UI_STATE = 'gridUIState'; - - static readonly CONTENT_AREA_SELECTOR = '.content-area'; - static readonly DATA_GRID_SELECTOR = '.datagrid'; - - /** - * ** Update strategy that will be used to update Browser URL. - * - * - 'updateLocation' will update softly update the URL using Location service, and it's default one - * - 'updateRouter' will trigger Angular router resolve mechanism with all guards and resolvers through Router service - */ - @Input() urlUpdateStrategy: 'updateLocation' | 'updateRouter' = 'updateLocation'; - - /** - * ** Query param key for search value. - */ - @Input() searchParam: string = QUERY_PARAM_SEARCH; - /** - * ** Position for search query param. - */ - @Input() searchParamPosition = 0; - - /** - * ** Base position index for Data Jobs filters query param. - * - * - Every filter has its own defined +x from the base. - */ - @Input() filtersQueryParamPositionBase = 0; - - /** - * ** URLStateManager external dependency injection to act in synchronous way external pages and the Data Jobs. - */ - @Input() set urlStateManager(value: URLStateManager) { - if (value) { - this._urlStateManager = value; - this._isUrlStateManagerExternalDependency = true; - } + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DataJobsBaseGridComponent"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "DataJobs-BaseGrid-Component"; + + static readonly UI_KEY_PAGE_OFFSET = "pageOffset"; + static readonly UI_KEY_GRID_OFFSET = "gridOffset"; + static readonly UI_KEY_GRID_UI_STATE = "gridUIState"; + + static readonly CONTENT_AREA_SELECTOR = ".content-area"; + static readonly DATA_GRID_SELECTOR = ".datagrid"; + + /** + * ** Update strategy that will be used to update Browser URL. + * + * - 'updateLocation' will update softly update the URL using Location service, and it's default one + * - 'updateRouter' will trigger Angular router resolve mechanism with all guards and resolvers through Router service + */ + @Input() urlUpdateStrategy: "updateLocation" | "updateRouter" = + "updateLocation"; + + /** + * ** Query param key for search value. + */ + @Input() searchParam: string = QUERY_PARAM_SEARCH; + /** + * ** Position for search query param. + */ + @Input() searchParamPosition = 0; + + /** + * ** Base position index for Data Jobs filters query param. + * + * - Every filter has its own defined +x from the base. + */ + @Input() filtersQueryParamPositionBase = 0; + + /** + * ** URLStateManager external dependency injection to act in synchronous way external pages and the Data Jobs. + */ + @Input() set urlStateManager(value: URLStateManager) { + if (value) { + this._urlStateManager = value; + this._isUrlStateManagerExternalDependency = true; + } + } + + get urlStateManager(): URLStateManager { + return this._urlStateManager; + } + + teamNameFilter: string; + displayMode = DisplayMode.STANDARD; + + filterByTeamName = false; + + selectedJob: DataJob; + gridState: ClrDatagridStateInterface; + loading = false; + + dataJobs: DataJob[] = []; + totalJobs = 0; + loadDataDebouncer = new Subject<"normal" | "forced">(); + + deploymentStatuses = [ + DataJobStatus.ENABLED, + DataJobStatus.DISABLED, + DataJobStatus.NOT_DEPLOYED, + ]; + executionStatuses = [ + DataJobExecutionStatus.SUCCEEDED, + DataJobExecutionStatus.PLATFORM_ERROR, + DataJobExecutionStatus.USER_ERROR, + DataJobExecutionStatus.SKIPPED, + DataJobExecutionStatus.CANCELLED, + ]; + + clrGridCurrentPage = 1; + clrGridUIState: ClrGridUIState; + clrGridDefaultFilter: ClrGridUIState["filter"]; + clrGridDefaultSort: ClrGridUIState["sort"]; + + quickFilters: QuickFilters; + + dataJobStatus = DataJobStatus; + + initializingComponent = true; + + /** + * ** Array of error code patterns that component should listen for in errors store. + */ + listenForErrorPatterns: string[] = [ + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE].All, + ]; + + /** + * ** Flag that indicates actionable elements should be disabled. + */ + disableActionableElements = false; + + protected restoreUIStateInProgress = false; + protected navigationInProgress = false; + protected _urlStateManager: URLStateManager; + + private _isUrlStateManagerExternalDependency = false; + + protected constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + protected readonly routerService: RouterService, + protected readonly dataJobsService: DataJobsService, + protected readonly dataJobsApiService: DataJobsApiService, + protected readonly errorHandlerService: ErrorHandlerService, + protected readonly location: Location, + protected readonly router: Router, + protected readonly elementRef: ElementRef, + protected readonly document: Document, + protected dataPipelinesModuleConfig: DataPipelinesConfig, + protected readonly localStorageConfigKey: string, + public localStorageUserConfig: DataJobsLocalStorageUserConfig, + className: string = null, + ) { + super( + componentService, + navigationService, + activatedRoute, + className ?? DataJobsBaseGridComponent.CLASS_NAME, + ); + + this._urlStateManager = new URLStateManager( + router.url.split("?")[0], + location, + ); + } + + /** + * ** NgFor elements tracking function. + */ + trackByFn(index: number, dataJob: DataJob): string { + return `${index}|${dataJob?.config?.team}|${dataJob?.jobName}`; + } + + resolveLogsUrl(job: DataJob): string { + if ( + CollectionsUtil.isNil(job) || + CollectionsUtil.isArrayEmpty(job.deployments) + ) { + return null; } - get urlStateManager(): URLStateManager { - return this._urlStateManager; + if (CollectionsUtil.isArrayEmpty(job.deployments[0].executions)) { + return null; } - teamNameFilter: string; - displayMode = DisplayMode.STANDARD; + return job.deployments[0].executions[0].logsUrl; + } - filterByTeamName = false; + showOrHideColumnChange(columnName: string, hidden: boolean): void { + this.localStorageUserConfig.hiddenColumns[columnName] = hidden; + localStorage.setItem( + this.localStorageConfigKey, + JSON.stringify(this.localStorageUserConfig), + ); + } - selectedJob: DataJob; - gridState: ClrDatagridStateInterface; - loading = false; + getJobStatus(job: DataJob): DataJobExecutionStatus { + if (job.deployments && job.deployments[0]?.lastExecutionStatus) { + return job.deployments[0]?.lastExecutionStatus; + } - dataJobs: DataJob[] = []; - totalJobs = 0; - loadDataDebouncer = new Subject<'normal' | 'forced'>(); + return null; + } - deploymentStatuses = [DataJobStatus.ENABLED, DataJobStatus.DISABLED, DataJobStatus.NOT_DEPLOYED]; - executionStatuses = [ - DataJobExecutionStatus.SUCCEEDED, - DataJobExecutionStatus.PLATFORM_ERROR, - DataJobExecutionStatus.USER_ERROR, - DataJobExecutionStatus.SKIPPED, - DataJobExecutionStatus.CANCELLED - ]; + getJobSuccessRateTitle(job: DataJob): string { + if (job.deployments) { + return `${job.deployments[0]?.successfulExecutions} successful / ${ + job.deployments[0]?.failedExecutions + + job.deployments[0]?.successfulExecutions + } total`; + } - clrGridCurrentPage = 1; - clrGridUIState: ClrGridUIState; - clrGridDefaultFilter: ClrGridUIState['filter']; - clrGridDefaultSort: ClrGridUIState['sort']; - - quickFilters: QuickFilters; - - dataJobStatus = DataJobStatus; - - initializingComponent = true; - - /** - * ** Array of error code patterns that component should listen for in errors store. - */ - listenForErrorPatterns: string[] = [LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE].All]; - - /** - * ** Flag that indicates actionable elements should be disabled. - */ - disableActionableElements = false; - - protected restoreUIStateInProgress = false; - protected navigationInProgress = false; - protected _urlStateManager: URLStateManager; - - private _isUrlStateManagerExternalDependency = false; - - protected constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - protected readonly routerService: RouterService, - protected readonly dataJobsService: DataJobsService, - protected readonly dataJobsApiService: DataJobsApiService, - protected readonly errorHandlerService: ErrorHandlerService, - protected readonly location: Location, - protected readonly router: Router, - protected readonly elementRef: ElementRef, - protected readonly document: Document, - protected dataPipelinesModuleConfig: DataPipelinesConfig, - protected readonly localStorageConfigKey: string, - public localStorageUserConfig: DataJobsLocalStorageUserConfig, - className: string = null - ) { - super(componentService, navigationService, activatedRoute, className ?? DataJobsBaseGridComponent.CLASS_NAME); + return null; + } - this._urlStateManager = new URLStateManager(router.url.split('?')[0], location); - } + /** + * ** Callback (listener) for User search. + */ + search(value: string) { + this.clrGridUIState.search = value; - /** - * ** NgFor elements tracking function. - */ - trackByFn(index: number, dataJob: DataJob): string { - return `${index}|${dataJob?.config?.team}|${dataJob?.jobName}`; - } + this._updateUrlStateManager(); - resolveLogsUrl(job: DataJob): string { - if (CollectionsUtil.isNil(job) || CollectionsUtil.isArrayEmpty(job.deployments)) { - return null; - } + this.refresh(); + } - if (CollectionsUtil.isArrayEmpty(job.deployments[0].executions)) { - return null; - } + refresh(): void { + this.loadDataWithState(null); + } - return job.deployments[0].executions[0].logsUrl; + /** + * ** Main callback (listener) for ClrGrid state mutation, like filters, sort. + */ + loadDataWithState(state: ClrDatagridStateInterface): void { + if (state != null) { + this.gridState = state; } - showOrHideColumnChange(columnName: string, hidden: boolean): void { - this.localStorageUserConfig.hiddenColumns[columnName] = hidden; - localStorage.setItem(this.localStorageConfigKey, JSON.stringify(this.localStorageUserConfig)); + if (!this.model || this.restoreUIStateInProgress) { + return; } - getJobStatus(job: DataJob): DataJobExecutionStatus { - if (job.deployments && job.deployments[0]?.lastExecutionStatus) { - return job.deployments[0]?.lastExecutionStatus; - } + if (this.filterByTeamName && !this.teamNameFilter) { + // While the teamNameFilter is empty, no refresh requests will be executed. + console.warn( + "Refresh operation will be skipped. teamNameFilter is empty.", + ); - return null; + return; } - getJobSuccessRateTitle(job: DataJob): string { - if (job.deployments) { - return `${job.deployments[0]?.successfulExecutions} successful / ${ - job.deployments[0]?.failedExecutions + job.deployments[0]?.successfulExecutions - } total`; - } + this.loadDataDebouncer.next("normal"); + } - return null; - } + isStandardDisplayMode() { + return this.displayMode === DisplayMode.STANDARD; + } - /** - * ** Callback (listener) for User search. - */ - search(value: string) { - this.clrGridUIState.search = value; + selectionChanged(dataJob: DataJob) { + this.selectedJob = dataJob; + } - this._updateUrlStateManager(); + /** + * ** Navigate to Data Job details page, while at first save Ui State of the Page. + */ + navigateToJobDetails(job?: DataJob) { + if (job) { + this.saveUIState(); + this.selectionChanged(job); - this.refresh(); - } + this.dataJobsService.notifyForTeamImplicitly(job.config?.team); + + this.navigationInProgress = true; - refresh(): void { - this.loadDataWithState(null); + this.navigateTo({ + "$.team": job.config?.team, + "$.job": job.jobName, + }).finally(() => { + this.navigationInProgress = false; + }); } + } + + /** + * @inheritDoc + */ + onModelInit(): void { + let initializationFinished = false; + let previousState: RouteState; + + this.subscriptions.push( + this.routerService + .get() + .pipe( + distinctUntilChanged( + (a, b) => + (a.state.absoluteConfigPath !== b.state.absoluteConfigPath || + a.state.absoluteRoutePath === b.state.absoluteRoutePath) && + this._areQueryParamsPristine(b.state), + ), + ) + .subscribe((routerState) => { + if (initializationFinished) { + // check if route state comes from Browser popped state (Browser stack) + if ( + (!previousState || + previousState.absoluteRoutePath === + routerState.state.absoluteRoutePath) && + !this._areQueryParamsPristine(routerState.state) + ) { + this._extractQueryParams(routerState.state); + this._updateUrlStateManager(); + + // set query params mutation to false, because it's Browser popped state + // no need to update the Browser URL, just URLStateManager need to be updated + this.urlStateManager.isQueryParamsStateMutated = false; + } else { + this._updateUrlStateManager(routerState.state); + } - /** - * ** Main callback (listener) for ClrGrid state mutation, like filters, sort. - */ - loadDataWithState(state: ClrDatagridStateInterface): void { - if (state != null) { - this.gridState = state; - } + previousState = routerState.state; - if (!this.model || this.restoreUIStateInProgress) { return; - } + } - if (this.filterByTeamName && !this.teamNameFilter) { - // While the teamNameFilter is empty, no refresh requests will be executed. - console.warn('Refresh operation will be skipped. teamNameFilter is empty.'); + initializationFinished = true; + previousState = routerState.state; - return; - } + this._initUrlStateManager(routerState.state); + this._extractQueryParams(routerState.state); - this.loadDataDebouncer.next('normal'); - } + if (this._doesRestoreUIStateExist()) { + if (this._shouldRestoreUIState(routerState)) { + this.restoreUIStateInProgress = true; - isStandardDisplayMode() { - return this.displayMode === DisplayMode.STANDARD; - } - - selectionChanged(dataJob: DataJob) { - this.selectedJob = dataJob; - } + const clrGridUIState = this.model.getUiState( + DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE, + ); + if (clrGridUIState) { + this.clrGridUIState = clrGridUIState; + } - /** - * ** Navigate to Data Job details page, while at first save Ui State of the Page. - */ - navigateToJobDetails(job?: DataJob) { - if (job) { - this.saveUIState(); - this.selectionChanged(job); + this.loadDataDebouncer.next("forced"); - this.dataJobsService.notifyForTeamImplicitly(job.config?.team); + return; + } else { + this._clearUiPageState(); + } + } + + if (this.gridState) { + this.refresh(); + } + }), + ); + } + + /** + * @inheritDoc + */ + onModelInitialLoad(): void { + this.routerService + .get() + .pipe(take(1)) + .subscribe((routerState) => { + if (this._shouldRestoreUIState(routerState)) { + this.restoreUIState(); + + this.restoreUIStateInProgress = false; + } + }); + } - this.navigationInProgress = true; + /** + * @inheritDoc + */ + onModelLoad(): void { + this.loading = false; - this.navigateTo({ - '$.team': job.config?.team, - '$.job': job.jobName - }).finally(() => { - this.navigationInProgress = false; - }); - } + if (this.initializingComponent) { + this.initializingComponent = false; } + } + + /** + * @inheritDoc + */ + onModelChange(model: ComponentModel): void { + this._extractData(model); + } + + /** + * @inheritDoc + */ + onModelError( + model: ComponentModel, + _task: string, + newErrorRecords: ErrorRecord[], + ): void { + this._extractData(model); + + newErrorRecords.forEach((errorRecord) => { + const error = ErrorUtil.extractError(errorRecord.error); + + this.errorHandlerService.processError(error); + }); + } + + /** + * @inheritDoc + */ + override ngOnInit(): void { + this._initializeClrGridUIState(); + + // attach listener to ErrorStore and listen for Errors change + this.errors.onChange((store) => { + // if there is record for listened error code patterns disable actionable elements + this.disableActionableElements = store.hasCodePattern( + ...this.listenForErrorPatterns, + ); + }); + + this.subscriptions.push( + this.loadDataDebouncer.pipe(debounceTime(300)).subscribe((handling) => { + if (this.isLoadDataAllowed() || handling === "forced") { + this._doLoadData(); + + this._initializeQuickFilters(); + this._updateUrlStateManager(); + + if (this.restoreUIStateInProgress) { + this._doUrlUpdate("replaceLocation"); + } + } - /** - * @inheritDoc - */ - onModelInit(): void { - let initializationFinished = false; - let previousState: RouteState; - - this.subscriptions.push( - this.routerService - .get() - .pipe( - distinctUntilChanged( - (a, b) => - (a.state.absoluteConfigPath !== b.state.absoluteConfigPath || - a.state.absoluteRoutePath === b.state.absoluteRoutePath) && - this._areQueryParamsPristine(b.state) - ) - ) - .subscribe((routerState) => { - if (initializationFinished) { - // check if route state comes from Browser popped state (Browser stack) - if ( - (!previousState || previousState.absoluteRoutePath === routerState.state.absoluteRoutePath) && - !this._areQueryParamsPristine(routerState.state) - ) { - this._extractQueryParams(routerState.state); - this._updateUrlStateManager(); - - // set query params mutation to false, because it's Browser popped state - // no need to update the Browser URL, just URLStateManager need to be updated - this.urlStateManager.isQueryParamsStateMutated = false; - } else { - this._updateUrlStateManager(routerState.state); - } - - previousState = routerState.state; - - return; - } - - initializationFinished = true; - previousState = routerState.state; - - this._initUrlStateManager(routerState.state); - this._extractQueryParams(routerState.state); - - if (this._doesRestoreUIStateExist()) { - if (this._shouldRestoreUIState(routerState)) { - this.restoreUIStateInProgress = true; - - const clrGridUIState = this.model.getUiState(DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE); - if (clrGridUIState) { - this.clrGridUIState = clrGridUIState; - } - - this.loadDataDebouncer.next('forced'); - - return; - } else { - this._clearUiPageState(); - } - } - - if (this.gridState) { - this.refresh(); - } - }) + if (this.isUrlUpdateAllowed() || handling === "forced") { + this._doUrlUpdate(); + } + }), + ); + + super.ngOnInit(); + + this.loading = true; + + try { + this._loadLocalStorageUserConfig(); + } catch (e1) { + console.error( + "Failed to read config from localStorage", + e1, + "Will attempt to re-create it.", + ); + try { + localStorage.removeItem(this.localStorageConfigKey); + this._loadLocalStorageUserConfig(); + } catch (e2) { + console.error( + "Was unable to re-initialize localStorage user config", + e2, ); + } } + } - /** - * @inheritDoc - */ - onModelInitialLoad(): void { - this.routerService - .get() - .pipe(take(1)) - .subscribe((routerState) => { - if (this._shouldRestoreUIState(routerState)) { - this.restoreUIState(); + protected isLoadDataAllowed(): boolean { + if (!this.gridState) { + //While the gridState is empty, no refresh requests will be executed. + console.log( + "Load data will be skipped. gridState is empty. operation not allowed.", + ); - this.restoreUIStateInProgress = false; - } - }); + return false; } - /** - * @inheritDoc - */ - onModelLoad(): void { - this.loading = false; - - if (this.initializingComponent) { - this.initializingComponent = false; - } + return !this.navigationInProgress; + } + + protected isUrlUpdateAllowed(): boolean { + return ( + !this.navigationInProgress && + this.urlStateManager.isQueryParamsStateMutated + ); + } + + protected saveUIState() { + const dataGrid = this.elementRef.nativeElement.querySelector( + DataJobsBaseGridComponent.DATA_GRID_SELECTOR, + ); + if (dataGrid) { + this.model.withUiState(DataJobsBaseGridComponent.UI_KEY_GRID_OFFSET, { + x: dataGrid.scrollLeft, + y: dataGrid.scrollTop, + }); } - /** - * @inheritDoc - */ - onModelChange(model: ComponentModel): void { - this._extractData(model); + const contentArea = this.document.querySelector( + DataJobsBaseGridComponent.CONTENT_AREA_SELECTOR, + ); + if (contentArea) { + this.model.withUiState(DataJobsBaseGridComponent.UI_KEY_PAGE_OFFSET, { + x: contentArea.scrollLeft, + y: contentArea.scrollTop, + }); } - /** - * @inheritDoc - */ - onModelError(model: ComponentModel, _task: string, newErrorRecords: ErrorRecord[]): void { - this._extractData(model); + const clrGridUIStateDeepCloned = CollectionsUtil.cloneDeep( + this.clrGridUIState, + ); + clrGridUIStateDeepCloned.pageSize = + this.model.getComponentState()?.page?.size; + clrGridUIStateDeepCloned.lastPage = this.clrGridCurrentPage; - newErrorRecords.forEach((errorRecord) => { - const error = ErrorUtil.extractError(errorRecord.error); + this.model.withUiState( + DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE, + clrGridUIStateDeepCloned, + ); - this.errorHandlerService.processError(error); - }); - } + this.componentService.update(this.model.getComponentState()); + } - /** - * @inheritDoc - */ - override ngOnInit(): void { - this._initializeClrGridUIState(); - - // attach listener to ErrorStore and listen for Errors change - this.errors.onChange((store) => { - // if there is record for listened error code patterns disable actionable elements - this.disableActionableElements = store.hasCodePattern(...this.listenForErrorPatterns); - }); - - this.subscriptions.push( - this.loadDataDebouncer.pipe(debounceTime(300)).subscribe((handling) => { - if (this.isLoadDataAllowed() || handling === 'forced') { - this._doLoadData(); - - this._initializeQuickFilters(); - this._updateUrlStateManager(); - - if (this.restoreUIStateInProgress) { - this._doUrlUpdate('replaceLocation'); - } - } - - if (this.isUrlUpdateAllowed() || handling === 'forced') { - this._doUrlUpdate(); - } - }) - ); - - super.ngOnInit(); - - this.loading = true; - - try { - this._loadLocalStorageUserConfig(); - } catch (e1) { - console.error('Failed to read config from localStorage', e1, 'Will attempt to re-create it.'); - try { - localStorage.removeItem(this.localStorageConfigKey); - this._loadLocalStorageUserConfig(); - } catch (e2) { - console.error('Was unable to re-initialize localStorage user config', e2); - } - } + protected restoreUIState() { + if (!this._doesRestoreUIStateExist()) { + return; } - protected isLoadDataAllowed(): boolean { - if (!this.gridState) { - //While the gridState is empty, no refresh requests will be executed. - console.log('Load data will be skipped. gridState is empty. operation not allowed.'); - - return false; - } - - return !this.navigationInProgress; + setTimeout(() => { + const gridOffset = this.model.getUiState( + DataJobsBaseGridComponent.UI_KEY_GRID_OFFSET, + ); + const dataGrid = this.elementRef.nativeElement.querySelector( + DataJobsBaseGridComponent.DATA_GRID_SELECTOR, + ); + if (dataGrid) { + dataGrid.scrollTo(gridOffset.x, gridOffset.y); + } + + const pageOffset = this.model.getUiState( + DataJobsBaseGridComponent.UI_KEY_PAGE_OFFSET, + ); + const contentArea = this.document.querySelector( + DataJobsBaseGridComponent.CONTENT_AREA_SELECTOR, + ); + if (contentArea) { + contentArea.scrollTo(pageOffset.x, pageOffset.y); + } + + this._clearUiPageState(); + }, 25); + } + + private _shouldRestoreUIState(routerState: RouterState): boolean { + const restoreUiWhen = + routerState.state.getData("restoreUiWhen"); + if (CollectionsUtil.isNil(restoreUiWhen)) { + return true; } - protected isUrlUpdateAllowed(): boolean { - return !this.navigationInProgress && this.urlStateManager.isQueryParamsStateMutated; + if (!CollectionsUtil.isString(restoreUiWhen.previousConfigPathLike)) { + return true; } - protected saveUIState() { - const dataGrid = this.elementRef.nativeElement.querySelector(DataJobsBaseGridComponent.DATA_GRID_SELECTOR); - if (dataGrid) { - this.model.withUiState(DataJobsBaseGridComponent.UI_KEY_GRID_OFFSET, { - x: dataGrid.scrollLeft, - y: dataGrid.scrollTop - }); - } - - const contentArea = this.document.querySelector(DataJobsBaseGridComponent.CONTENT_AREA_SELECTOR); - if (contentArea) { - this.model.withUiState(DataJobsBaseGridComponent.UI_KEY_PAGE_OFFSET, { - x: contentArea.scrollLeft, - y: contentArea.scrollTop - }); - } - - const clrGridUIStateDeepCloned = CollectionsUtil.cloneDeep(this.clrGridUIState); - clrGridUIStateDeepCloned.pageSize = this.model.getComponentState()?.page?.size; - clrGridUIStateDeepCloned.lastPage = this.clrGridCurrentPage; - - this.model.withUiState(DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE, clrGridUIStateDeepCloned); - - this.componentService.update(this.model.getComponentState()); + return routerState + .getPrevious() + .state.absoluteConfigPath.includes(restoreUiWhen.previousConfigPathLike); + } + + private _doesRestoreUIStateExist(): boolean { + return ( + CollectionsUtil.isDefined(this.model) && + CollectionsUtil.isDefined( + this.model.getUiState( + DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE, + ), + ) + ); + } + + private _clearUiPageState() { + this.model + .getComponentState() + .uiState.delete(DataJobsBaseGridComponent.UI_KEY_GRID_OFFSET); + this.model + .getComponentState() + .uiState.delete(DataJobsBaseGridComponent.UI_KEY_PAGE_OFFSET); + this.model + .getComponentState() + .uiState.delete(DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE); + + this.componentService.update(this.model.getComponentState()); + } + + private _doLoadData(): void { + this.selectedJob = null; + this.loading = true; + + if (this._doesRestoreUIStateExist()) { + this.clrGridCurrentPage = this.clrGridUIState.lastPage; + } else { + this.model + .withFilter(this._buildRefreshFilters()) + .withSearch(this.clrGridUIState.search) + .withPage(this.gridState?.page?.current, this.gridState?.page?.size); } - protected restoreUIState() { - if (!this._doesRestoreUIStateExist()) { - return; - } + this.dataJobsService.loadJobs(this.model); + } - setTimeout(() => { - const gridOffset = this.model.getUiState(DataJobsBaseGridComponent.UI_KEY_GRID_OFFSET); - const dataGrid = this.elementRef.nativeElement.querySelector(DataJobsBaseGridComponent.DATA_GRID_SELECTOR); - if (dataGrid) { - dataGrid.scrollTo(gridOffset.x, gridOffset.y); - } + private _extractData(model: ComponentModel): void { + const componentState = model.getComponentState(); + const dataJobsData: { content?: DataJob[]; totalItems?: number } = + componentState.data.get(JOBS_DATA_KEY) ?? {}; - const pageOffset = this.model.getUiState(DataJobsBaseGridComponent.UI_KEY_PAGE_OFFSET); - const contentArea = this.document.querySelector(DataJobsBaseGridComponent.CONTENT_AREA_SELECTOR); - if (contentArea) { - contentArea.scrollTo(pageOffset.x, pageOffset.y); - } + this.dataJobs = CollectionsUtil.isArray(dataJobsData?.content) + ? [...dataJobsData?.content] + : []; - this._clearUiPageState(); - }, 25); - } + this.clrGridUIState.totalItems = dataJobsData?.totalItems ?? 0; + } - private _shouldRestoreUIState(routerState: RouterState): boolean { - const restoreUiWhen = routerState.state.getData('restoreUiWhen'); - if (CollectionsUtil.isNil(restoreUiWhen)) { - return true; - } + private _initUrlStateManager(routeState: RouteState): void { + if (!this._isUrlStateManagerExternalDependency) { + this._urlStateManager = new URLStateManager( + routeState.absoluteRoutePath, + this.location, + ); + } + } - if (!CollectionsUtil.isString(restoreUiWhen.previousConfigPathLike)) { - return true; - } + private _extractQueryParams(routeState: RouteState): void { + if (!routeState.queryParams) { + this.clrGridUIState.search = ""; + this.clrGridUIState.filter = {}; - return routerState.getPrevious().state.absoluteConfigPath.includes(restoreUiWhen.previousConfigPathLike); + return; } - private _doesRestoreUIStateExist(): boolean { - return ( - CollectionsUtil.isDefined(this.model) && - CollectionsUtil.isDefined(this.model.getUiState(DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE)) + if (!this.initializingComponent) { + this.clrGridUIState.filter.jobName = routeState.getQueryParam("jobName"); + this.clrGridUIState.filter.teamName = + routeState.getQueryParam("teamName"); + this.clrGridUIState.filter.description = + routeState.getQueryParam("description"); + this.clrGridUIState.filter.deploymentStatus = + this._decodeFilterFromQueryParam( + "deploymentStatus", + routeState.getQueryParam("deploymentStatus"), ); + this.clrGridUIState.filter.deploymentLastExecutionStatus = + this._decodeFilterFromQueryParam( + "deploymentLastExecutionStatus", + routeState.getQueryParam("deploymentLastExecutionStatus"), + ); + } else { + this._checkMutatedFilterAndUpdate(routeState, "jobName", false); + this._checkMutatedFilterAndUpdate(routeState, "teamName", false); + this._checkMutatedFilterAndUpdate(routeState, "description", false); + + this._checkMutatedFilterAndUpdate(routeState, "deploymentStatus", true); + this._checkMutatedFilterAndUpdate( + routeState, + "deploymentLastExecutionStatus", + true, + ); } - private _clearUiPageState() { - this.model.getComponentState().uiState.delete(DataJobsBaseGridComponent.UI_KEY_GRID_OFFSET); - this.model.getComponentState().uiState.delete(DataJobsBaseGridComponent.UI_KEY_PAGE_OFFSET); - this.model.getComponentState().uiState.delete(DataJobsBaseGridComponent.UI_KEY_GRID_UI_STATE); - - this.componentService.update(this.model.getComponentState()); + // search has different handling so because of that is last handled + const searchQueryString = routeState.getQueryParam(this.searchParam); + const normalizedSearchQueryString = searchQueryString + ? searchQueryString + : ""; + if (this.clrGridUIState.search !== normalizedSearchQueryString) { + this.search(normalizedSearchQueryString); } + } - private _doLoadData(): void { - this.selectedJob = null; - this.loading = true; - - if (this._doesRestoreUIStateExist()) { - this.clrGridCurrentPage = this.clrGridUIState.lastPage; - } else { - this.model - .withFilter(this._buildRefreshFilters()) - .withSearch(this.clrGridUIState.search) - .withPage(this.gridState?.page?.current, this.gridState?.page?.size); - } - - this.dataJobsService.loadJobs(this.model); + private _updateUrlStateManager(routeState?: RouteState): void { + if (CollectionsUtil.isDefined(routeState)) { + this.urlStateManager.baseURL = routeState.absoluteRoutePath; } - private _extractData(model: ComponentModel): void { - const componentState = model.getComponentState(); - const dataJobsData: { content?: DataJob[]; totalItems?: number } = componentState.data.get(JOBS_DATA_KEY) ?? {}; - - this.dataJobs = CollectionsUtil.isArray(dataJobsData?.content) ? [...dataJobsData?.content] : []; - - this.clrGridUIState.totalItems = dataJobsData?.totalItems ?? 0; + this.urlStateManager.setQueryParam( + "jobName", + this.clrGridUIState.filter.jobName, + this.filtersQueryParamPositionBase + 1, + ); + this.urlStateManager.setQueryParam( + "teamName", + this.clrGridUIState.filter.teamName, + this.filtersQueryParamPositionBase + 2, + ); + this.urlStateManager.setQueryParam( + "description", + this.clrGridUIState.filter.description, + this.filtersQueryParamPositionBase + 3, + ); + this.urlStateManager.setQueryParam( + "deploymentStatus", + this._encodeFilterForQueryParam( + "deploymentStatus", + this.clrGridUIState.filter.deploymentStatus, + ), + this.filtersQueryParamPositionBase + 4, + ); + this.urlStateManager.setQueryParam( + "deploymentLastExecutionStatus", + this._encodeFilterForQueryParam( + "deploymentLastExecutionStatus", + this.clrGridUIState.filter.deploymentLastExecutionStatus, + ), + this.filtersQueryParamPositionBase + 5, + ); + + // search has different handling so because of that is last handled + this.urlStateManager.setQueryParam( + this.searchParam, + this.clrGridUIState.search, + this.searchParamPosition, + ); + } + + private _areQueryParamsPristine(routeState: RouteState): boolean { + if ( + this.clrGridUIState.search !== routeState.getQueryParam(this.searchParam) + ) { + return false; } - private _initUrlStateManager(routeState: RouteState): void { - if (!this._isUrlStateManagerExternalDependency) { - this._urlStateManager = new URLStateManager(routeState.absoluteRoutePath, this.location); - } + if ( + this.clrGridUIState.filter.jobName !== routeState.getQueryParam("jobName") + ) { + return false; } - private _extractQueryParams(routeState: RouteState): void { - if (!routeState.queryParams) { - this.clrGridUIState.search = ''; - this.clrGridUIState.filter = {}; - - return; - } - - if (!this.initializingComponent) { - this.clrGridUIState.filter.jobName = routeState.getQueryParam('jobName'); - this.clrGridUIState.filter.teamName = routeState.getQueryParam('teamName'); - this.clrGridUIState.filter.description = routeState.getQueryParam('description'); - this.clrGridUIState.filter.deploymentStatus = this._decodeFilterFromQueryParam( - 'deploymentStatus', - routeState.getQueryParam('deploymentStatus') - ); - this.clrGridUIState.filter.deploymentLastExecutionStatus = this._decodeFilterFromQueryParam( - 'deploymentLastExecutionStatus', - routeState.getQueryParam('deploymentLastExecutionStatus') - ); - } else { - this._checkMutatedFilterAndUpdate(routeState, 'jobName', false); - this._checkMutatedFilterAndUpdate(routeState, 'teamName', false); - this._checkMutatedFilterAndUpdate(routeState, 'description', false); - - this._checkMutatedFilterAndUpdate(routeState, 'deploymentStatus', true); - this._checkMutatedFilterAndUpdate(routeState, 'deploymentLastExecutionStatus', true); - } - - // search has different handling so because of that is last handled - const searchQueryString = routeState.getQueryParam(this.searchParam); - const normalizedSearchQueryString = searchQueryString ? searchQueryString : ''; - if (this.clrGridUIState.search !== normalizedSearchQueryString) { - this.search(normalizedSearchQueryString); - } + if ( + this.clrGridUIState.filter.teamName !== + routeState.getQueryParam("teamName") + ) { + return false; } - private _updateUrlStateManager(routeState?: RouteState): void { - if (CollectionsUtil.isDefined(routeState)) { - this.urlStateManager.baseURL = routeState.absoluteRoutePath; - } - - this.urlStateManager.setQueryParam('jobName', this.clrGridUIState.filter.jobName, this.filtersQueryParamPositionBase + 1); - this.urlStateManager.setQueryParam('teamName', this.clrGridUIState.filter.teamName, this.filtersQueryParamPositionBase + 2); - this.urlStateManager.setQueryParam('description', this.clrGridUIState.filter.description, this.filtersQueryParamPositionBase + 3); - this.urlStateManager.setQueryParam( - 'deploymentStatus', - this._encodeFilterForQueryParam('deploymentStatus', this.clrGridUIState.filter.deploymentStatus), - this.filtersQueryParamPositionBase + 4 - ); - this.urlStateManager.setQueryParam( - 'deploymentLastExecutionStatus', - this._encodeFilterForQueryParam('deploymentLastExecutionStatus', this.clrGridUIState.filter.deploymentLastExecutionStatus), - this.filtersQueryParamPositionBase + 5 - ); - - // search has different handling so because of that is last handled - this.urlStateManager.setQueryParam(this.searchParam, this.clrGridUIState.search, this.searchParamPosition); + if ( + this.clrGridUIState.filter.description !== + routeState.getQueryParam("description") + ) { + return false; } - private _areQueryParamsPristine(routeState: RouteState): boolean { - if (this.clrGridUIState.search !== routeState.getQueryParam(this.searchParam)) { - return false; - } - - if (this.clrGridUIState.filter.jobName !== routeState.getQueryParam('jobName')) { - return false; - } - - if (this.clrGridUIState.filter.teamName !== routeState.getQueryParam('teamName')) { - return false; - } - - if (this.clrGridUIState.filter.description !== routeState.getQueryParam('description')) { - return false; - } - - if ( - this.clrGridUIState.filter.deploymentStatus !== - this._decodeFilterFromQueryParam('deploymentStatus', routeState.getQueryParam('deploymentStatus')) - ) { - return false; - } + if ( + this.clrGridUIState.filter.deploymentStatus !== + this._decodeFilterFromQueryParam( + "deploymentStatus", + routeState.getQueryParam("deploymentStatus"), + ) + ) { + return false; + } - return ( - this.clrGridUIState.filter.deploymentLastExecutionStatus === - this._decodeFilterFromQueryParam('deploymentLastExecutionStatus', routeState.getQueryParam('deploymentLastExecutionStatus')) + return ( + this.clrGridUIState.filter.deploymentLastExecutionStatus === + this._decodeFilterFromQueryParam( + "deploymentLastExecutionStatus", + routeState.getQueryParam("deploymentLastExecutionStatus"), + ) + ); + } + + private _checkMutatedFilterAndUpdate( + routeState: RouteState, + key: keyof GridFilters, + decode: boolean, + ): void { + if (!decode) { + if ( + CollectionsUtil.isDefined(routeState.getQueryParam(key)) && + this.clrGridUIState.filter[key] !== routeState.getQueryParam(key) + ) { + this.clrGridUIState.filter[key] = routeState.getQueryParam(key); + } + } else { + if ( + CollectionsUtil.isDefined( + routeState.getQueryParam(key) && + this.clrGridUIState.filter[key] !== + this._decodeFilterFromQueryParam( + key, + routeState.getQueryParam(key), + ), + ) + ) { + this.clrGridUIState.filter[key] = this._decodeFilterFromQueryParam( + key, + routeState.getQueryParam(key), ); + } } - - private _checkMutatedFilterAndUpdate(routeState: RouteState, key: keyof GridFilters, decode: boolean): void { - if (!decode) { - if ( - CollectionsUtil.isDefined(routeState.getQueryParam(key)) && - this.clrGridUIState.filter[key] !== routeState.getQueryParam(key) - ) { - this.clrGridUIState.filter[key] = routeState.getQueryParam(key); - } - } else { - if ( - CollectionsUtil.isDefined( - routeState.getQueryParam(key) && - this.clrGridUIState.filter[key] !== this._decodeFilterFromQueryParam(key, routeState.getQueryParam(key)) - ) - ) { - this.clrGridUIState.filter[key] = this._decodeFilterFromQueryParam(key, routeState.getQueryParam(key)); - } - } + } + + private _doUrlUpdate( + strategy: "updateLocation" | "updateRouter" | "replaceLocation" = this + .urlUpdateStrategy, + ): void { + if (strategy === "updateLocation") { + this.urlStateManager.locationToURL(); + } else if (strategy === "updateRouter") { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.urlStateManager.navigateToUrl().then(); + } else { + this.urlStateManager.replaceToUrl(); } - - private _doUrlUpdate(strategy: 'updateLocation' | 'updateRouter' | 'replaceLocation' = this.urlUpdateStrategy): void { - if (strategy === 'updateLocation') { - this.urlStateManager.locationToURL(); - } else if (strategy === 'updateRouter') { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.urlStateManager.navigateToUrl().then(); - } else { - this.urlStateManager.replaceToUrl(); - } + } + + private _loadLocalStorageUserConfig() { + const userConfig = localStorage.getItem(this.localStorageConfigKey); + if (userConfig) { + let newColumnProvided = false; + const parsedUserConfig: DataJobsLocalStorageUserConfig = + JSON.parse(userConfig); + + CollectionsUtil.iterateObject( + this.localStorageUserConfig.hiddenColumns, + (value, key) => { + if (!parsedUserConfig.hiddenColumns.hasOwnProperty(key)) { + newColumnProvided = true; + parsedUserConfig.hiddenColumns[key] = value; + } + }, + ); + + if (newColumnProvided) { + localStorage.setItem( + this.localStorageConfigKey, + JSON.stringify(parsedUserConfig), + ); + } + + this.localStorageUserConfig = parsedUserConfig; + } else { + localStorage.setItem( + this.localStorageConfigKey, + JSON.stringify(this.localStorageUserConfig), + ); + } + } + + /** + * ** Builds refresh filters. + * + * - Convert filters from an array to map, because that's what backend-calling service is expecting + */ + private _buildRefreshFilters(): ApiPredicate[] { + const filters: ApiPredicate[] = []; + + if (this.teamNameFilter) { + filters.push({ + property: "config.team", + pattern: this.teamNameFilter, + sort: null, + }); } - private _loadLocalStorageUserConfig() { - const userConfig = localStorage.getItem(this.localStorageConfigKey); - if (userConfig) { - let newColumnProvided = false; - const parsedUserConfig: DataJobsLocalStorageUserConfig = JSON.parse(userConfig); + if (this.gridState?.filters) { + for (const _filter of this.gridState.filters) { + const { property, value } = _filter as { + property: string; + value: string; + }; - CollectionsUtil.iterateObject(this.localStorageUserConfig.hiddenColumns, (value, key) => { - if (!parsedUserConfig.hiddenColumns.hasOwnProperty(key)) { - newColumnProvided = true; - parsedUserConfig.hiddenColumns[key] = value; - } - }); + filters.push({ + property, + pattern: this._createApiFilterPattern(property, value), + sort: null, + }); + } + } - if (newColumnProvided) { - localStorage.setItem(this.localStorageConfigKey, JSON.stringify(parsedUserConfig)); - } + if (this.gridState?.sort) { + const direction = this.gridState.sort.reverse ? DESC : ASC; - this.localStorageUserConfig = parsedUserConfig; - } else { - localStorage.setItem(this.localStorageConfigKey, JSON.stringify(this.localStorageUserConfig)); - } + filters.push({ + property: this.gridState.sort.by as string, + pattern: null, + sort: direction, + }); } - /** - * ** Builds refresh filters. - * - * - Convert filters from an array to map, because that's what backend-calling service is expecting - */ - private _buildRefreshFilters(): ApiPredicate[] { - const filters: ApiPredicate[] = []; - - if (this.teamNameFilter) { - filters.push({ - property: 'config.team', - pattern: this.teamNameFilter, - sort: null - }); + return filters; + } + + private _encodeFilterForQueryParam( + propertyName: keyof GridFilters, + value: string, + ): string { + switch (propertyName) { + case "deploymentStatus": + if (CollectionsUtil.isNil(value)) { + return "all"; } - if (this.gridState?.filters) { - for (const _filter of this.gridState.filters) { - const { property, value } = _filter as { - property: string; - value: string; - }; - - filters.push({ - property, - pattern: this._createApiFilterPattern(property, value), - sort: null - }); - } + return `${value}`.replace(" ", "_").toLowerCase(); + case "deploymentLastExecutionStatus": + if (CollectionsUtil.isNil(value)) { + return undefined; } - if (this.gridState?.sort) { - const direction = this.gridState.sort.reverse ? DESC : ASC; - - filters.push({ - property: this.gridState.sort.by as string, - pattern: null, - sort: direction - }); - } - - return filters; + return `${value}`.toLowerCase(); + default: + return `${value}`.toLowerCase(); } - - private _encodeFilterForQueryParam(propertyName: keyof GridFilters, value: string): string { - switch (propertyName) { - case 'deploymentStatus': - if (CollectionsUtil.isNil(value)) { - return 'all'; - } - - return `${value}`.replace(' ', '_').toLowerCase(); - case 'deploymentLastExecutionStatus': - if (CollectionsUtil.isNil(value)) { - return undefined; - } - - return `${value}`.toLowerCase(); - default: - return `${value}`.toLowerCase(); + } + + private _decodeFilterFromQueryParam( + propertyName: keyof GridFilters, + value: string, + ): string { + switch (propertyName) { + case "deploymentStatus": + switch (value) { + case "enabled": + return DataJobStatus.ENABLED; + case "disabled": + return DataJobStatus.DISABLED; + case "not_deployed": + return DataJobStatus.NOT_DEPLOYED; + default: + return undefined; } - } - - private _decodeFilterFromQueryParam(propertyName: keyof GridFilters, value: string): string { - switch (propertyName) { - case 'deploymentStatus': - switch (value) { - case 'enabled': - return DataJobStatus.ENABLED; - case 'disabled': - return DataJobStatus.DISABLED; - case 'not_deployed': - return DataJobStatus.NOT_DEPLOYED; - default: - return undefined; - } - case 'deploymentLastExecutionStatus': - if (CollectionsUtil.isNil(value)) { - return undefined; - } - - const normalizedExecStatus: DataJobExecutionStatus = `${value}`.toUpperCase() as DataJobExecutionStatus; - - return this.executionStatuses.includes(normalizedExecStatus) ? normalizedExecStatus : undefined; - default: - return `${value}`.toLowerCase(); + case "deploymentLastExecutionStatus": + if (CollectionsUtil.isNil(value)) { + return undefined; } - } - private _createApiFilterPattern(propertyName: string, value: string) { - // TODO: Remove this, once the Backend support % filterting for all the properties - // TODO: Once jobName get the same handling as config.team, add case proper case - switch (propertyName) { - case 'config.team': - return `%${value}%`; - case 'deployments.enabled': - return `${value}`.toLowerCase().replace(' ', '_'); - case 'deployments.lastExecutionStatus': - return `${value}`.toLowerCase(); - case 'jobName': - return `*${value}*`; - default: - return `${value}`; - } - } + const normalizedExecStatus: DataJobExecutionStatus = + `${value}`.toUpperCase() as DataJobExecutionStatus; - private _initializeQuickFilters(): void { - const activateFilter = (status: DataJobStatus) => () => { - this.clrGridUIState.filter.deploymentStatus = status; - }; + return this.executionStatuses.includes(normalizedExecStatus) + ? normalizedExecStatus + : undefined; + default: + return `${value}`.toLowerCase(); + } + } + + private _createApiFilterPattern(propertyName: string, value: string) { + // TODO: Remove this, once the Backend support % filterting for all the properties + // TODO: Once jobName get the same handling as config.team, add case proper case + switch (propertyName) { + case "config.team": + return `%${value}%`; + case "deployments.enabled": + return `${value}`.toLowerCase().replace(" ", "_"); + case "deployments.lastExecutionStatus": + return `${value}`.toLowerCase(); + case "jobName": + return `*${value}*`; + default: + return `${value}`; + } + } - const deactivateFilter = () => { - delete this.clrGridUIState.filter.deploymentStatus; - }; + private _initializeQuickFilters(): void { + const activateFilter = (status: DataJobStatus) => () => { + this.clrGridUIState.filter.deploymentStatus = status; + }; - const isActiveQuickFilter = (status: DataJobStatus | 'all'): boolean => { - if (status === 'all') { - return CollectionsUtil.isNil(this.clrGridUIState.filter.deploymentStatus); - } + const deactivateFilter = () => { + delete this.clrGridUIState.filter.deploymentStatus; + }; - return this.clrGridUIState.filter.deploymentStatus === status; - }; + const isActiveQuickFilter = (status: DataJobStatus | "all"): boolean => { + if (status === "all") { + return CollectionsUtil.isNil( + this.clrGridUIState.filter.deploymentStatus, + ); + } + + return this.clrGridUIState.filter.deploymentStatus === status; + }; + + const filters: QuickFilters = [ + { + label: "All", + suppressCancel: true, + active: isActiveQuickFilter("all"), + onActivate: deactivateFilter, + }, + { + label: "Enabled", + active: isActiveQuickFilter(DataJobStatus.ENABLED), + onActivate: activateFilter(DataJobStatus.ENABLED), + onDeactivate: deactivateFilter, + icon: { + title: "Enabled - This job is deployed and executed by schedule", + class: "is-solid status-icon-enabled", + shape: "check-circle", + size: 20, + }, + }, + { + label: "Disabled", + active: isActiveQuickFilter(DataJobStatus.DISABLED), + onActivate: activateFilter(DataJobStatus.DISABLED), + onDeactivate: deactivateFilter, + icon: { + title: + "Disabled - This job is deployed but not executing by schedule", + class: "is-solid status-icon-disabled", + shape: "times-circle", + size: 15, + }, + }, + { + label: "Not Deployed", + active: isActiveQuickFilter(DataJobStatus.NOT_DEPLOYED), + onActivate: activateFilter(DataJobStatus.NOT_DEPLOYED), + onDeactivate: deactivateFilter, + icon: { + title: "Not Deployed - This job is created but still not deployed", + shape: "circle", + size: 15, + }, + }, + ]; - const filters: QuickFilters = [ - { - label: 'All', - suppressCancel: true, - active: isActiveQuickFilter('all'), - onActivate: deactivateFilter - }, - { - label: 'Enabled', - active: isActiveQuickFilter(DataJobStatus.ENABLED), - onActivate: activateFilter(DataJobStatus.ENABLED), - onDeactivate: deactivateFilter, - icon: { - title: 'Enabled - This job is deployed and executed by schedule', - class: 'is-solid status-icon-enabled', - shape: 'check-circle', - size: 20 - } - }, - { - label: 'Disabled', - active: isActiveQuickFilter(DataJobStatus.DISABLED), - onActivate: activateFilter(DataJobStatus.DISABLED), - onDeactivate: deactivateFilter, - icon: { - title: 'Disabled - This job is deployed but not executing by schedule', - class: 'is-solid status-icon-disabled', - shape: 'times-circle', - size: 15 - } - }, - { - label: 'Not Deployed', - active: isActiveQuickFilter(DataJobStatus.NOT_DEPLOYED), - onActivate: activateFilter(DataJobStatus.NOT_DEPLOYED), - onDeactivate: deactivateFilter, - icon: { - title: 'Not Deployed - This job is created but still not deployed', - shape: 'circle', - size: 15 - } - } - ]; - - this.quickFilters = filters; - } - - private _initializeClrGridUIState(): void { - this.clrGridUIState = { - totalItems: 0, - lastPage: 1, - pageSize: 25, - filter: { - ...(this.clrGridDefaultFilter ?? {}) - }, - sort: { - ...(this.clrGridDefaultSort ?? {}) - }, - search: '' - }; - } + this.quickFilters = filters; + } + + private _initializeClrGridUIState(): void { + this.clrGridUIState = { + totalItems: 0, + lastPage: 1, + pageSize: 25, + filter: { + ...(this.clrGridDefaultFilter ?? {}), + }, + sort: { + ...(this.clrGridDefaultSort ?? {}), + }, + search: "", + }; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.html index 6c09d4b00f..01c163096d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.html @@ -6,230 +6,216 @@
-
-
-
+
+
+

+ + + + Data Job: {{ jobName }} +

+
+ +
+

+ -

- - - - Data Job: {{ jobName }} -

-

- -
-

- - - - Data Job: {{ jobName }} -

-
-
+ + + Data Job: {{ jobName }} +
+ +
-
- - +
+ + - - + + - - + + - - - -
+ + + +
- + - + - -
+ +
- + Executions + + + -
- -
+
+ +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.scss index 5a01a6aea8..0ee558e508 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.scss @@ -4,115 +4,115 @@ */ @mixin fill-parent-content() { - display: flex; - flex-direction: column; - flex: 1 1 auto; + display: flex; + flex-direction: column; + flex: 1 1 auto; } hr { - border: 0; - height: 1px; - background: #8f9ba3; + border: 0; + height: 1px; + background: #8f9ba3; } .label-link { - cursor: pointer; + cursor: pointer; } .label-link-suppress-decoration { - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; + } } .status-icon-enabled { - color: hsl(93, 67%, 38%); + color: hsl(93, 67%, 38%); } .clr-col-6 { - border-right: 1px solid #999999; + border-right: 1px solid #999999; } .loading-spinner { - margin-left: 45%; - margin-top: 100px; + margin-left: 45%; + margin-top: 100px; } :host { - @include fill-parent-content(); - height: 100%; + @include fill-parent-content(); + height: 100%; } .data-pipelines-job__page { - @include fill-parent-content(); - - .data-pipelines-job__actions { - display: inline-flex; - margin-top: 0.8rem; + @include fill-parent-content(); - .data-pipelines-job__actions-right { - margin-left: auto; + .data-pipelines-job__actions { + display: inline-flex; + margin-top: 0.8rem; - clr-dropdown { - margin-top: var(--clr-btn-vertical-margin, 0.3rem); - margin-bottom: var(--clr-btn-vertical-margin, 0.3rem); - margin-left: 0; - } + .data-pipelines-job__actions-right { + margin-left: auto; - .data-pipelines-job__action-dropdown-trigger { - padding-right: 1.5rem; + clr-dropdown { + margin-top: var(--clr-btn-vertical-margin, 0.3rem); + margin-bottom: var(--clr-btn-vertical-margin, 0.3rem); + margin-left: 0; + } - clr-icon { - transform: translateY(-50%) rotate(180deg); - top: 0.85rem; - } - } - } + .data-pipelines-job__action-dropdown-trigger { + padding-right: 1.5rem; - &.data-pipelines-job__actions--margin-0 { - margin: 0; + clr-icon { + transform: translateY(-50%) rotate(180deg); + top: 0.85rem; } + } + } - .data-pipelines-job__navigate-back { - margin-right: 10px; - margin-top: -1px; + &.data-pipelines-job__actions--margin-0 { + margin: 0; + } - .redo-icon { - color: var(--clr-link-color, #0072a3); - height: 25px; - width: 25px; - } - } + .data-pipelines-job__navigate-back { + margin-right: 10px; + margin-top: -1px; - .data-pipelines-job__info { - display: inline-flex; + .redo-icon { + color: var(--clr-link-color, #0072a3); + height: 25px; + width: 25px; + } + } - span { - margin-right: 0.5rem; - } - } + .data-pipelines-job__info { + display: inline-flex; - .page-title { - span { - font-size: x-large; - } - } + span { + margin-right: 0.5rem; + } } - .data-pipelines-job__tabs-navigation { - margin-top: 7px; + .page-title { + span { + font-size: x-large; + } + } + } - .job-details__promotion-icon { - margin-top: -0.75rem; - margin-left: 0.25rem; - } + .data-pipelines-job__tabs-navigation { + margin-top: 7px; - .beta-icon { - vertical-align: baseline; - } + .job-details__promotion-icon { + margin-top: -0.75rem; + margin-left: 0.25rem; } - .data-pipelines-job__router-outlet-container { - @include fill-parent-content(); + .beta-icon { + vertical-align: baseline; } + } + + .data-pipelines-job__router-outlet-container { + @include fill-parent-content(); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.spec.ts index 89f5a223c7..f1dfae27fe 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.spec.ts @@ -3,291 +3,323 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { RouterTestingModule } from "@angular/router/testing"; -import { BehaviorSubject, of, Subject } from 'rxjs'; +import { BehaviorSubject, of, Subject } from "rxjs"; import { - ComponentModel, - ComponentService, - ComponentStateImpl, - createRouteSnapshot, - ErrorHandlerService, - NavigationService, - RouterService, - RouterState, - RouteState, - ToastService -} from '@versatiledatakit/shared'; + ComponentModel, + ComponentService, + ComponentStateImpl, + createRouteSnapshot, + ErrorHandlerService, + NavigationService, + RouterService, + RouterState, + RouteState, + ToastService, +} from "@versatiledatakit/shared"; import { - DATA_PIPELINES_CONFIGS, - DataJobDeploymentDetails, - DataJobDeploymentStatus, - DataJobDetails, - DataJobExecution, - DataJobExecutionStatus, - DataJobExecutionType -} from '../../model'; - -import { DataJobsApiService, DataJobsService } from '../../services'; - -import { DataJobPageComponent } from './data-job-page.component'; - -describe('DataJobsDetailsComponent', () => { - let componentServiceStub: jasmine.SpyObj; - let navigationServiceStub: jasmine.SpyObj; - let routerServiceStub: jasmine.SpyObj; - let toastServiceStub: jasmine.SpyObj; - let dataJobsApiServiceStub: jasmine.SpyObj; - let dataJobsServiceStub: jasmine.SpyObj; - let errorHandlerServiceStub: jasmine.SpyObj; - - let componentModelStub: ComponentModel; - let component: DataJobPageComponent; - let fixture: ComponentFixture; - - const TEST_JOB_EXECUTION = { - id: 'id002', - jobName: 'job002', - status: DataJobExecutionStatus.SUBMITTED, - startTime: new Date().toISOString(), - startedBy: 'aUserov', - endTime: new Date().toISOString(), - type: DataJobExecutionType.MANUAL, - opId: 'op002', - message: 'message001', - deployment: { - id: 'id002', - enabled: true, - jobVersion: '002', - mode: 'test_mode', - vdkVersion: '002', - resources: { - memoryLimit: 1000, - memoryRequest: 1000, - cpuLimit: 0.5, - cpuRequest: 0.5 - }, - executions: [], - deployedDate: '2020-11-11T10:10:10Z', - deployedBy: 'pmitev', - status: DataJobDeploymentStatus.SUCCESS - } - } as DataJobExecution; - - const TEST_JOB_DEPLOYMENT = { - id: 'id002', - enabled: true, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - job_version: '002', - mode: 'test_mode', - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - vdk_version: '002', - python_version: '3.9-secure', - endTime: new Date(), - opId: 'op002', - message: 'message002', - /* eslint-disable @typescript-eslint/naming-convention */ - deployed_date: '2020-11-11T10:10:10Z', - deployed_by: 'pmitev', - resources: { - memory_limit: 1000, - memory_request: 1000, - cpu_limit: 0.5, - cpu_request: 0.5 - } - /* eslint-enable @typescript-eslint/naming-convention */ - } as DataJobDeploymentDetails; - - beforeEach(() => { - componentServiceStub = jasmine.createSpyObj('componentService', ['init', 'getModel', 'idle']); - navigationServiceStub = jasmine.createSpyObj('navigationService', ['navigate', 'navigateTo', 'navigateBack']); - routerServiceStub = jasmine.createSpyObj('routerService', ['getState']); - toastServiceStub = jasmine.createSpyObj('toastService', ['show']); - dataJobsApiServiceStub = jasmine.createSpyObj('dataJobsApiService', [ - 'getJobDetails', - 'getJobExecutions', - 'getJobDeployments', - 'removeJob', - 'downloadFile', - 'executeDataJob', - 'getJob' - ]); - dataJobsServiceStub = jasmine.createSpyObj('dataJobsService', [ - 'loadJobs', - 'notifyForRunningJobExecutionId', - 'notifyForJobExecutions', - 'notifyForTeamImplicitly', - 'getNotifiedForRunningJobExecutionId', - 'getNotifiedForJobExecutions', - 'getNotifiedForTeamImplicitly' - ]); - errorHandlerServiceStub = jasmine.createSpyObj('errorHandlerService', ['processError', 'handleError']); - - const activatedRouteStub = () => ({ - snapshot: createRouteSnapshot({ - data: { - activateSubpageNavigation: true - } - }) - }); - - dataJobsApiServiceStub.executeDataJob.and.returnValue(new Subject()); - dataJobsApiServiceStub.getJob.and.returnValue(new Subject()); - dataJobsApiServiceStub.getJobDetails.and.returnValue(new Subject()); - dataJobsApiServiceStub.getJobExecutions.and.returnValue(of({ content: [TEST_JOB_EXECUTION], totalItems: 1, totalPages: 1 })); - dataJobsApiServiceStub.getJobDeployments.and.returnValue(of([TEST_JOB_DEPLOYMENT])); - dataJobsApiServiceStub.removeJob.and.returnValue(new Subject()); - dataJobsApiServiceStub.downloadFile.and.returnValue(new Subject()); - - dataJobsServiceStub.getNotifiedForRunningJobExecutionId.and.returnValue(new Subject()); - dataJobsServiceStub.getNotifiedForJobExecutions.and.returnValue(new Subject()); - dataJobsServiceStub.getNotifiedForTeamImplicitly.and.returnValue(new BehaviorSubject('taurus')); - - TestBed.configureTestingModule({ - imports: [RouterTestingModule.withRoutes([])], - schemas: [NO_ERRORS_SCHEMA], - declarations: [DataJobPageComponent], - providers: [ - { provide: ComponentService, useValue: componentServiceStub }, - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: RouterService, useValue: routerServiceStub }, - { provide: ActivatedRoute, useFactory: activatedRouteStub }, - { provide: ToastService, useValue: toastServiceStub }, - { - provide: DataJobsApiService, - useValue: dataJobsApiServiceStub - }, - { provide: DataJobsService, useValue: dataJobsServiceStub }, - { - provide: ErrorHandlerService, - useValue: errorHandlerServiceStub - }, - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => ({ - defaultOwnerTeamName: 'all', - manageConfig: { - allowKeyTabDownloads: true, - allowExecuteNow: true - } - }) - } - ] - }); - - componentModelStub = ComponentModel.of(ComponentStateImpl.of({}), RouterState.of(RouteState.empty(), 1)); - componentServiceStub.init.and.returnValue(of(componentModelStub)); - componentServiceStub.getModel.and.returnValue(of(componentModelStub)); - routerServiceStub.getState.and.returnValue(of(RouteState.empty())); - - fixture = TestBed.createComponent(DataJobPageComponent); - component = fixture.componentInstance; - component.model = componentModelStub; + DATA_PIPELINES_CONFIGS, + DataJobDeploymentDetails, + DataJobDeploymentStatus, + DataJobDetails, + DataJobExecution, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../model"; + +import { DataJobsApiService, DataJobsService } from "../../services"; + +import { DataJobPageComponent } from "./data-job-page.component"; + +describe("DataJobsDetailsComponent", () => { + let componentServiceStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let routerServiceStub: jasmine.SpyObj; + let toastServiceStub: jasmine.SpyObj; + let dataJobsApiServiceStub: jasmine.SpyObj; + let dataJobsServiceStub: jasmine.SpyObj; + let errorHandlerServiceStub: jasmine.SpyObj; + + let componentModelStub: ComponentModel; + let component: DataJobPageComponent; + let fixture: ComponentFixture; + + const TEST_JOB_EXECUTION = { + id: "id002", + jobName: "job002", + status: DataJobExecutionStatus.SUBMITTED, + startTime: new Date().toISOString(), + startedBy: "aUserov", + endTime: new Date().toISOString(), + type: DataJobExecutionType.MANUAL, + opId: "op002", + message: "message001", + deployment: { + id: "id002", + enabled: true, + jobVersion: "002", + mode: "test_mode", + vdkVersion: "002", + resources: { + memoryLimit: 1000, + memoryRequest: 1000, + cpuLimit: 0.5, + cpuRequest: 0.5, + }, + executions: [], + deployedDate: "2020-11-11T10:10:10Z", + deployedBy: "pmitev", + status: DataJobDeploymentStatus.SUCCESS, + }, + } as DataJobExecution; + + const TEST_JOB_DEPLOYMENT = { + id: "id002", + enabled: true, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + job_version: "002", + mode: "test_mode", + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + vdk_version: "002", + python_version: "3.9-secure", + endTime: new Date(), + opId: "op002", + message: "message002", + /* eslint-disable @typescript-eslint/naming-convention */ + deployed_date: "2020-11-11T10:10:10Z", + deployed_by: "pmitev", + resources: { + memory_limit: 1000, + memory_request: 1000, + cpu_limit: 0.5, + cpu_request: 0.5, + }, + /* eslint-enable @typescript-eslint/naming-convention */ + } as DataJobDeploymentDetails; + + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["init", "getModel", "idle"], + ); + navigationServiceStub = jasmine.createSpyObj( + "navigationService", + ["navigate", "navigateTo", "navigateBack"], + ); + routerServiceStub = jasmine.createSpyObj("routerService", [ + "getState", + ]); + toastServiceStub = jasmine.createSpyObj("toastService", [ + "show", + ]); + dataJobsApiServiceStub = jasmine.createSpyObj( + "dataJobsApiService", + [ + "getJobDetails", + "getJobExecutions", + "getJobDeployments", + "removeJob", + "downloadFile", + "executeDataJob", + "getJob", + ], + ); + dataJobsServiceStub = jasmine.createSpyObj( + "dataJobsService", + [ + "loadJobs", + "notifyForRunningJobExecutionId", + "notifyForJobExecutions", + "notifyForTeamImplicitly", + "getNotifiedForRunningJobExecutionId", + "getNotifiedForJobExecutions", + "getNotifiedForTeamImplicitly", + ], + ); + errorHandlerServiceStub = jasmine.createSpyObj( + "errorHandlerService", + ["processError", "handleError"], + ); + + const activatedRouteStub = () => ({ + snapshot: createRouteSnapshot({ + data: { + activateSubpageNavigation: true, + }, + }), }); - it('can load instance', () => { - expect(component).toBeTruthy(); + dataJobsApiServiceStub.executeDataJob.and.returnValue(new Subject()); + dataJobsApiServiceStub.getJob.and.returnValue(new Subject()); + dataJobsApiServiceStub.getJobDetails.and.returnValue(new Subject()); + dataJobsApiServiceStub.getJobExecutions.and.returnValue( + of({ content: [TEST_JOB_EXECUTION], totalItems: 1, totalPages: 1 }), + ); + dataJobsApiServiceStub.getJobDeployments.and.returnValue( + of([TEST_JOB_DEPLOYMENT]), + ); + dataJobsApiServiceStub.removeJob.and.returnValue(new Subject()); + dataJobsApiServiceStub.downloadFile.and.returnValue(new Subject()); + + dataJobsServiceStub.getNotifiedForRunningJobExecutionId.and.returnValue( + new Subject(), + ); + dataJobsServiceStub.getNotifiedForJobExecutions.and.returnValue( + new Subject(), + ); + dataJobsServiceStub.getNotifiedForTeamImplicitly.and.returnValue( + new BehaviorSubject("taurus"), + ); + + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes([])], + schemas: [NO_ERRORS_SCHEMA], + declarations: [DataJobPageComponent], + providers: [ + { provide: ComponentService, useValue: componentServiceStub }, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ActivatedRoute, useFactory: activatedRouteStub }, + { provide: ToastService, useValue: toastServiceStub }, + { + provide: DataJobsApiService, + useValue: dataJobsApiServiceStub, + }, + { provide: DataJobsService, useValue: dataJobsServiceStub }, + { + provide: ErrorHandlerService, + useValue: errorHandlerServiceStub, + }, + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => ({ + defaultOwnerTeamName: "all", + manageConfig: { + allowKeyTabDownloads: true, + allowExecuteNow: true, + }, + }), + }, + ], }); - describe('ngOnInit', () => { - it('makes expected calls', () => { - // When - component.ngOnInit(); - - // Then - expect(componentServiceStub.init).toHaveBeenCalled(); - expect(componentServiceStub.getModel).toHaveBeenCalled(); - expect(routerServiceStub.getState).toHaveBeenCalled(); - }); + componentModelStub = ComponentModel.of( + ComponentStateImpl.of({}), + RouterState.of(RouteState.empty(), 1), + ); + componentServiceStub.init.and.returnValue(of(componentModelStub)); + componentServiceStub.getModel.and.returnValue(of(componentModelStub)); + routerServiceStub.getState.and.returnValue(of(RouteState.empty())); + + fixture = TestBed.createComponent(DataJobPageComponent); + component = fixture.componentInstance; + component.model = componentModelStub; + }); + + it("can load instance", () => { + expect(component).toBeTruthy(); + }); + + describe("ngOnInit", () => { + it("makes expected calls", () => { + // When + component.ngOnInit(); + + // Then + expect(componentServiceStub.init).toHaveBeenCalled(); + expect(componentServiceStub.getModel).toHaveBeenCalled(); + expect(routerServiceStub.getState).toHaveBeenCalled(); }); + }); - describe('allowJobExecuteNow', () => { - it('returns true in case of dataPipelinesModuleConfig.manageConfig.allowExecuteNow', () => { - expect(component.isExecuteJobAllowed).toBeFalse(); - }); + describe("allowJobExecuteNow", () => { + it("returns true in case of dataPipelinesModuleConfig.manageConfig.allowExecuteNow", () => { + expect(component.isExecuteJobAllowed).toBeFalse(); }); - - describe('confirmExecuteJob', () => { - it('makes expected calls', () => { - // When - // @ts-ignore - spyOn(component, '_submitOperationStarted').and.callThrough(); - // @ts-ignore - spyOn(component, '_extractJobDeployment').and.returnValue({ - id: '10' - } as DataJobDetails); - - // When - component.confirmExecuteJob(); - - // Then - expect(dataJobsApiServiceStub.executeDataJob).toHaveBeenCalled(); - // @ts-ignore - expect(component._submitOperationStarted).toHaveBeenCalled(); - }); + }); + + describe("confirmExecuteJob", () => { + it("makes expected calls", () => { + // When + // @ts-ignore + spyOn(component, "_submitOperationStarted").and.callThrough(); + // @ts-ignore + spyOn(component, "_extractJobDeployment").and.returnValue({ + id: "10", + } as DataJobDetails); + + // When + component.confirmExecuteJob(); + + // Then + expect(dataJobsApiServiceStub.executeDataJob).toHaveBeenCalled(); + // @ts-ignore + expect(component._submitOperationStarted).toHaveBeenCalled(); }); - - describe('executeJob', () => { - it('sets executeNowOptions', () => { - // When - component.executeJob(); - - // Then - expect(component.executeNowOptions.message).toBeDefined(); - expect(component.executeNowOptions.infoText).toBeDefined(); - expect(component.executeNowOptions.opened).toBeTrue(); - expect(component.executeNowOptions.title).toBeDefined(); - }); + }); + + describe("executeJob", () => { + it("sets executeNowOptions", () => { + // When + component.executeJob(); + + // Then + expect(component.executeNowOptions.message).toBeDefined(); + expect(component.executeNowOptions.infoText).toBeDefined(); + expect(component.executeNowOptions.opened).toBeTrue(); + expect(component.executeNowOptions.title).toBeDefined(); }); + }); - describe('downloadKey', () => { - it('makes expected calls', () => { - // Given - // @ts-ignore - spyOn(component, '_submitOperationStarted').and.callThrough(); + describe("downloadKey", () => { + it("makes expected calls", () => { + // Given + // @ts-ignore + spyOn(component, "_submitOperationStarted").and.callThrough(); - // When - component.downloadJobKey(); + // When + component.downloadJobKey(); - // Then - // @ts-ignore - expect(component._submitOperationStarted).toHaveBeenCalled(); - expect(dataJobsApiServiceStub.downloadFile).toHaveBeenCalled(); - }); + // Then + // @ts-ignore + expect(component._submitOperationStarted).toHaveBeenCalled(); + expect(dataJobsApiServiceStub.downloadFile).toHaveBeenCalled(); }); + }); - describe('confirmRemoveJob', () => { - it('makes expected calls', () => { - // Given - // @ts-ignore - spyOn(component, '_submitOperationStarted').and.callThrough(); + describe("confirmRemoveJob", () => { + it("makes expected calls", () => { + // Given + // @ts-ignore + spyOn(component, "_submitOperationStarted").and.callThrough(); - // When - component.confirmRemoveJob(); + // When + component.confirmRemoveJob(); - // Then - // @ts-ignore - expect(component._submitOperationStarted).toHaveBeenCalled(); - expect(dataJobsApiServiceStub.removeJob).toHaveBeenCalled(); - }); + // Then + // @ts-ignore + expect(component._submitOperationStarted).toHaveBeenCalled(); + expect(dataJobsApiServiceStub.removeJob).toHaveBeenCalled(); }); - - describe('removeJob', () => { - it('sets deleteOptions', () => { - // When - component.removeJob(); - - // Then - expect(component.deleteOptions.message).toBeDefined(); - expect(component.deleteOptions.infoText).toBeDefined(); - expect(component.deleteOptions.showOkBtn).toBeTrue(); - expect(component.deleteOptions.cancelBtn).toBeDefined(); - expect(component.deleteOptions.opened).toBeTrue(); - }); + }); + + describe("removeJob", () => { + it("sets deleteOptions", () => { + // When + component.removeJob(); + + // Then + expect(component.deleteOptions.message).toBeDefined(); + expect(component.deleteOptions.infoText).toBeDefined(); + expect(component.deleteOptions.showOkBtn).toBeTrue(); + expect(component.deleteOptions.cancelBtn).toBeDefined(); + expect(component.deleteOptions.opened).toBeTrue(); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.ts index 35f4e13d9b..417e85dd7a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/data-job-page.component.ts @@ -5,641 +5,748 @@ /* eslint-disable @typescript-eslint/member-ordering */ -import { Component, Inject, OnInit } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; -import { HttpErrorResponse } from '@angular/common/http'; +import { Component, Inject, OnInit } from "@angular/core"; +import { ActivatedRoute, Params } from "@angular/router"; +import { HttpErrorResponse } from "@angular/common/http"; -import { concatMap, interval, of, Subject, timer } from 'rxjs'; -import { catchError, filter, finalize, map, switchMap, take, takeUntil, takeWhile, tap } from 'rxjs/operators'; +import { concatMap, interval, of, Subject, timer } from "rxjs"; +import { + catchError, + filter, + finalize, + map, + switchMap, + take, + takeUntil, + takeWhile, + tap, +} from "rxjs/operators"; -import { ClrLoadingState } from '@clr/angular'; +import { ClrLoadingState } from "@clr/angular"; -import * as fileSaver from 'file-saver'; +import * as fileSaver from "file-saver"; import { - ASC, - CollectionsUtil, - ComponentModel, - ComponentService, - ErrorHandlerService, - ErrorRecord, - NavigationService, - OnTaurusModelError, - OnTaurusModelInit, - RouterService, - RouteState, - TaurusBaseComponent, - ToastService, - VmwToastType -} from '@versatiledatakit/shared'; - -import { DataJobUtil, ErrorUtil } from '../../shared/utils'; -import { ExtractJobStatusPipe } from '../../shared/pipes'; -import { ConfirmationModalOptions, DeleteModalOptions, ModalOptions } from '../../shared/model'; + ASC, + CollectionsUtil, + ComponentModel, + ComponentService, + ErrorHandlerService, + ErrorRecord, + NavigationService, + OnTaurusModelError, + OnTaurusModelInit, + RouterService, + RouteState, + TaurusBaseComponent, + ToastService, + VmwToastType, +} from "@versatiledatakit/shared"; + +import { DataJobUtil, ErrorUtil } from "../../shared/utils"; +import { ExtractJobStatusPipe } from "../../shared/pipes"; +import { + ConfirmationModalOptions, + DeleteModalOptions, + ModalOptions, +} from "../../shared/model"; import { - DATA_PIPELINES_CONFIGS, - DataJobDeployment, - DataJobExecution, - DataJobExecutionDetails, - DataJobExecutions, - DataJobExecutionsPage, - DataJobStatus, - DataPipelinesConfig, - ToastDefinitions -} from '../../model'; - -import { DataJobsApiService, DataJobsService } from '../../services'; + DATA_PIPELINES_CONFIGS, + DataJobDeployment, + DataJobExecution, + DataJobExecutionDetails, + DataJobExecutions, + DataJobExecutionsPage, + DataJobStatus, + DataPipelinesConfig, + ToastDefinitions, +} from "../../model"; + +import { DataJobsApiService, DataJobsService } from "../../services"; enum TypeButtonState { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - DOWNLOAD, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - EXECUTE, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - DELETE, - /* eslint-disabe-next-line @typescript-eslint/naming-convention */ - STOP + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + DOWNLOAD, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + EXECUTE, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + DELETE, + /* eslint-disabe-next-line @typescript-eslint/naming-convention */ + STOP, } @Component({ - selector: 'lib-data-job-page', - templateUrl: './data-job-page.component.html', - styleUrls: ['./data-job-page.component.scss'] + selector: "lib-data-job-page", + templateUrl: "./data-job-page.component.html", + styleUrls: ["./data-job-page.component.scss"], }) -export class DataJobPageComponent extends TaurusBaseComponent implements OnInit, OnTaurusModelInit, OnTaurusModelError { - readonly uuid = 'DataJobPageComponent'; - - teamName = ''; - jobName = ''; - isDataJobRunning = false; - cancelDataJobDisabled = false; - - queryParams: Params = {}; - - isSubpageNavigation = false; - - isJobAvailable = false; - isJobEditable = false; - - isExecuteJobAllowed = false; - isDownloadJobKeyAllowed = false; - - areJobExecutionsLoaded = false; - - loadingInProgress = false; - - jobExecutions: DataJobExecutions = []; - jobDeployments: DataJobDeployment[] = []; - - deleteButtonsState = ClrLoadingState.DEFAULT; - executeButtonsState = ClrLoadingState.DEFAULT; - downloadButtonsState = ClrLoadingState.DEFAULT; - stopButtonsState = ClrLoadingState.DEFAULT; - - deleteOptions: ModalOptions; - executeNowOptions: ModalOptions; - cancelNowOptions: ModalOptions; - - private _nonExistingJobMsgShowed = false; - - constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - private readonly routerService: RouterService, - private readonly dataJobsService: DataJobsService, - private readonly dataJobsApiService: DataJobsApiService, - private readonly toastService: ToastService, - private readonly errorHandlerService: ErrorHandlerService, - @Inject(DATA_PIPELINES_CONFIGS) - public readonly dataPipelinesModuleConfig: DataPipelinesConfig - ) { - super(componentService, navigationService, activatedRoute); - - this.isSubpageNavigation = !!activatedRoute.snapshot.data['activateSubpageNavigation']; +export class DataJobPageComponent + extends TaurusBaseComponent + implements OnInit, OnTaurusModelInit, OnTaurusModelError +{ + readonly uuid = "DataJobPageComponent"; + + teamName = ""; + jobName = ""; + isDataJobRunning = false; + cancelDataJobDisabled = false; + + queryParams: Params = {}; + + isSubpageNavigation = false; + + isJobAvailable = false; + isJobEditable = false; + + isExecuteJobAllowed = false; + isDownloadJobKeyAllowed = false; + + areJobExecutionsLoaded = false; + + loadingInProgress = false; + + jobExecutions: DataJobExecutions = []; + jobDeployments: DataJobDeployment[] = []; + + deleteButtonsState = ClrLoadingState.DEFAULT; + executeButtonsState = ClrLoadingState.DEFAULT; + downloadButtonsState = ClrLoadingState.DEFAULT; + stopButtonsState = ClrLoadingState.DEFAULT; + + deleteOptions: ModalOptions; + executeNowOptions: ModalOptions; + cancelNowOptions: ModalOptions; + + private _nonExistingJobMsgShowed = false; + + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + private readonly routerService: RouterService, + private readonly dataJobsService: DataJobsService, + private readonly dataJobsApiService: DataJobsApiService, + private readonly toastService: ToastService, + private readonly errorHandlerService: ErrorHandlerService, + @Inject(DATA_PIPELINES_CONFIGS) + public readonly dataPipelinesModuleConfig: DataPipelinesConfig, + ) { + super(componentService, navigationService, activatedRoute); + + this.isSubpageNavigation = + !!activatedRoute.snapshot.data["activateSubpageNavigation"]; + + this.deleteOptions = new DeleteModalOptions(); + this.executeNowOptions = new ConfirmationModalOptions(); + this.cancelNowOptions = new ConfirmationModalOptions(); + } + + /** + * ** Navigate back leveraging provided router config. + */ + doNavigateBack($event?: MouseEvent): void { + $event?.preventDefault(); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.navigateBack({ "$.team": this.teamName }).then(); + } + + /** + * ** Returns if execution is in progress. + */ + isExecutionInProgress(): boolean { + return DataJobUtil.isJobRunning(this.jobExecutions); + } + + /** + * ** Show confirmation dialog for Job execution. + */ + executeJob() { + this.executeNowOptions.title = `Execute ${this.jobName} now?`; + this.executeNowOptions.message = `Job ${this.jobName} will be queued for execution.`; + this.executeNowOptions.infoText = `Confirming will result in immediate data job execution.`; + this.executeNowOptions.opened = true; + } + + /** + * ** On User confirm continue with Job execution. + */ + confirmExecuteJob() { + this._submitOperationStarted(TypeButtonState.EXECUTE); + + this.subscriptions.push( + this.dataJobsApiService + .executeDataJob( + this.teamName, + this.jobName, + this._extractJobDeployment()?.id, + ) + .pipe( + finalize(() => { + this._submitOperationEnded(); + }), + ) + .subscribe({ + next: () => { + this.toastService.show( + ToastDefinitions.successfullyRanJob(this.jobName), + ); - this.deleteOptions = new DeleteModalOptions(); - this.executeNowOptions = new ConfirmationModalOptions(); - this.cancelNowOptions = new ConfirmationModalOptions(); - } + let previousReqFinished = true; - /** - * ** Navigate back leveraging provided router config. - */ - doNavigateBack($event?: MouseEvent): void { - $event?.preventDefault(); + this.areJobExecutionsLoaded = false; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.navigateBack({ '$.team': this.teamName }).then(); - } - - /** - * ** Returns if execution is in progress. - */ - isExecutionInProgress(): boolean { - return DataJobUtil.isJobRunning(this.jobExecutions); - } + this.subscriptions.push( + interval(1250) // Send polling request on every 1.25s until execution is accepted from backend + .pipe( + // eslint-disable-next-line rxjs/no-unsafe-takeuntil + takeUntil(timer(30000)), // Timer limit when polling to stop = 30s + filter(() => previousReqFinished), + tap(() => (previousReqFinished = false)), + switchMap(() => + this.dataJobsApiService + .getJobExecutions( + this.teamName, + this.jobName, + true, + null, + { + property: "startTime", + direction: ASC, + }, + ) + .pipe( + catchError((error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + ); + + return of([]); + }), + finalize(() => { + previousReqFinished = true; + }), + ), + ), + map((executions: DataJobExecutionsPage) => + executions.content ? [...executions.content] : [], + ), + takeWhile((executions) => { + if ( + CollectionsUtil.isArrayEmpty(executions) || + executions.length <= this.jobExecutions.length + ) { + return true; + } - /** - * ** Show confirmation dialog for Job execution. - */ - executeJob() { - this.executeNowOptions.title = `Execute ${this.jobName} now?`; - this.executeNowOptions.message = `Job ${this.jobName} will be queued for execution.`; - this.executeNowOptions.infoText = `Confirming will result in immediate data job execution.`; - this.executeNowOptions.opened = true; - } + this.jobExecutions = executions; - /** - * ** On User confirm continue with Job execution. - */ - confirmExecuteJob() { - this._submitOperationStarted(TypeButtonState.EXECUTE); + this.areJobExecutionsLoaded = true; - this.subscriptions.push( - this.dataJobsApiService - .executeDataJob(this.teamName, this.jobName, this._extractJobDeployment()?.id) - .pipe( - finalize(() => { - this._submitOperationEnded(); - }) - ) - .subscribe({ - next: () => { - this.toastService.show(ToastDefinitions.successfullyRanJob(this.jobName)); - - let previousReqFinished = true; - - this.areJobExecutionsLoaded = false; - - this.subscriptions.push( - interval(1250) // Send polling request on every 1.25s until execution is accepted from backend - .pipe( - // eslint-disable-next-line rxjs/no-unsafe-takeuntil - takeUntil(timer(30000)), // Timer limit when polling to stop = 30s - filter(() => previousReqFinished), - tap(() => (previousReqFinished = false)), - switchMap(() => - this.dataJobsApiService - .getJobExecutions(this.teamName, this.jobName, true, null, { - property: 'startTime', - direction: ASC - }) - .pipe( - catchError((error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error)); - - return of([]); - }), - finalize(() => { - previousReqFinished = true; - }) - ) - ), - map((executions: DataJobExecutionsPage) => (executions.content ? [...executions.content] : [])), - takeWhile((executions) => { - if (CollectionsUtil.isArrayEmpty(executions) || executions.length <= this.jobExecutions.length) { - return true; - } - - this.jobExecutions = executions; - - this.areJobExecutionsLoaded = true; - - const lastExecution = executions[executions.length - 1]; - if (!DataJobUtil.isJobRunningPredicate(lastExecution)) { - return true; - } - - this.dataJobsService.notifyForJobExecutions(executions); - this.dataJobsService.notifyForRunningJobExecutionId(lastExecution.id); - - return false; // Stop polling if above condition is met. - }) - ) - .subscribe() // eslint-disable-line rxjs/no-nested-subscribe - ); - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - title: - (error as HttpErrorResponse)?.status === 409 - ? 'Failed, Data job is already executing' - : 'Failed to queue Data job for execution' - }); + const lastExecution = executions[executions.length - 1]; + if (!DataJobUtil.isJobRunningPredicate(lastExecution)) { + return true; } - }) - ); - } - /** - * ** Download Job key. - */ - downloadJobKey() { - this._submitOperationStarted(TypeButtonState.DOWNLOAD); - - this.dataJobsApiService - .downloadFile(this.teamName, this.jobName) - .pipe( - finalize(() => { - this._submitOperationEnded(); - }) - ) - .subscribe({ - next: (response: Blob) => { - const blob: Blob = new Blob([response], { - type: 'application/octet-stream' - }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - fileSaver.saveAs(blob, `${this.jobName}.keytab`); - - this.toastService.show({ - type: VmwToastType.INFO, - title: `Download completed`, - description: `Data job keytab "${this.jobName}.keytab" successfully downloaded` - }); - }, - error: (error: unknown) => { - const errorDescription = - (error as HttpErrorResponse)?.status === 404 - ? `Download failed. Keytab file doesn't exist for this job.` - : `Download failed. Keytab file failed to download.`; - - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - description: errorDescription - }); - } - }); - } + this.dataJobsService.notifyForJobExecutions(executions); + this.dataJobsService.notifyForRunningJobExecutionId( + lastExecution.id, + ); - /** - * ** Show confirmation dialog for Job Remove (Delete). - */ - removeJob() { - this.deleteOptions.message = `Job ${this.jobName} will be deleted. + return false; // Stop polling if above condition is met. + }), + ) + .subscribe(), // eslint-disable-line rxjs/no-nested-subscribe + ); + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + title: + (error as HttpErrorResponse)?.status === 409 + ? "Failed, Data job is already executing" + : "Failed to queue Data job for execution", + }, + ); + }, + }), + ); + } + + /** + * ** Download Job key. + */ + downloadJobKey() { + this._submitOperationStarted(TypeButtonState.DOWNLOAD); + + this.dataJobsApiService + .downloadFile(this.teamName, this.jobName) + .pipe( + finalize(() => { + this._submitOperationEnded(); + }), + ) + .subscribe({ + next: (response: Blob) => { + const blob: Blob = new Blob([response], { + type: "application/octet-stream", + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + fileSaver.saveAs(blob, `${this.jobName}.keytab`); + + this.toastService.show({ + type: VmwToastType.INFO, + title: `Download completed`, + description: `Data job keytab "${this.jobName}.keytab" successfully downloaded`, + }); + }, + error: (error: unknown) => { + const errorDescription = + (error as HttpErrorResponse)?.status === 404 + ? `Download failed. Keytab file doesn't exist for this job.` + : `Download failed. Keytab file failed to download.`; + + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + description: errorDescription, + }, + ); + }, + }); + } + + /** + * ** Show confirmation dialog for Job Remove (Delete). + */ + removeJob() { + this.deleteOptions.message = `Job ${this.jobName} will be deleted. Currently executing Data Jobs will be left to finish but the credentials will be revoked.`; - this.deleteOptions.infoText = `Deleting this job means that it will be permanently removed from the system + this.deleteOptions.infoText = `Deleting this job means that it will be permanently removed from the system including all its state (properties), source code and any deployments.`; - this.deleteOptions.showOkBtn = true; - this.deleteOptions.cancelBtn = 'Cancel'; - this.deleteOptions.opened = true; - } - - /** - * ** On User confirm continue with Job Remove (Delete). - */ - confirmRemoveJob() { - this._submitOperationStarted(TypeButtonState.DELETE); - - this.dataJobsApiService - .removeJob(this.teamName, this.jobName) - .pipe( - finalize(() => { - this._submitOperationEnded(); - }) - ) - .subscribe({ - next: () => { - this.toastService.show({ - type: VmwToastType.INFO, - title: `Data job delete completed`, - description: `Data job "${this.jobName}" successfully deleted` - }); - - this.doNavigateBack(); - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - title: `Data job delete failed` - }); - } - }); - } - - confirmCancelDataJob() { - this._submitOperationStarted(TypeButtonState.STOP); - this.dataJobsApiService - .cancelDataJobExecution(this.teamName, this.jobName, this.lastExecution()?.id) - .pipe( - finalize(() => { - this._submitOperationEnded(); - }) - ) - .subscribe({ - next: () => { - this.cancelDataJobDisabled = true; - this.toastService.show({ - type: VmwToastType.INFO, - title: `Data job execution cancellation completed`, - description: `Data job "${this.jobName}" successfully canceled` - }); - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - title: `Data job cancellation failed` - }); - } - }); - } - - /** - * ** Show confirmation dialog for Job execution cancellation. - */ - cancelExecution() { - this.cancelNowOptions.title = `Cancel ${this.lastExecution()?.id} now?`; - this.cancelNowOptions.message = `Execution ${this.lastExecution()?.id} will be canceled.`; - this.cancelNowOptions.infoText = `Confirming will result in immediate data job execution cancellation.`; - this.cancelNowOptions.opened = true; - } - - lastExecution(): DataJobExecution { - return this.jobExecutions[this.jobExecutions.length - 1]; - } - - isJobWithRunningStatus(): boolean { - return this.lastExecution().status === 'RUNNING'; - } - - /** - * @inheritDoc - */ - onModelInit(): void { - this.routerService - .getState() - .pipe(take(1)) - .subscribe((state) => this._initialize(state)); - } - - /** - * @inheritDoc - */ - onModelError(model: ComponentModel, _task: string, newErrorRecords: ErrorRecord[]) { - newErrorRecords.forEach((errorRecord) => { - const error = ErrorUtil.extractError(errorRecord.error); - - this.errorHandlerService.processError(error); - }); - } - - private _initialize(state: RouteState): void { - const teamParamKey = state.getData('teamParamKey'); - this.teamName = state.getParam(teamParamKey); - - if (CollectionsUtil.isNil(teamParamKey) || CollectionsUtil.isNil(this.teamName)) { - this._subscribeForImplicitTeam(); - } - - const jobParamKey = state.getData('jobParamKey'); - this.jobName = state.getParam(jobParamKey); - - this.isJobEditable = !!state.getData('editable'); - - this.queryParams = state.queryParams; - - this.isDownloadJobKeyAllowed = this.dataPipelinesModuleConfig.manageConfig?.allowKeyTabDownloads && this.isJobEditable; - - this._subscribeForTeamChange(state); - this._subscribeForExecutionsChange(); - this._subscribeForExecutionIdChange(); - this._loadJobDetails(); - this._loadJobExecutions(); + this.deleteOptions.showOkBtn = true; + this.deleteOptions.cancelBtn = "Cancel"; + this.deleteOptions.opened = true; + } + + /** + * ** On User confirm continue with Job Remove (Delete). + */ + confirmRemoveJob() { + this._submitOperationStarted(TypeButtonState.DELETE); + + this.dataJobsApiService + .removeJob(this.teamName, this.jobName) + .pipe( + finalize(() => { + this._submitOperationEnded(); + }), + ) + .subscribe({ + next: () => { + this.toastService.show({ + type: VmwToastType.INFO, + title: `Data job delete completed`, + description: `Data job "${this.jobName}" successfully deleted`, + }); + + this.doNavigateBack(); + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + title: `Data job delete failed`, + }, + ); + }, + }); + } + + confirmCancelDataJob() { + this._submitOperationStarted(TypeButtonState.STOP); + this.dataJobsApiService + .cancelDataJobExecution( + this.teamName, + this.jobName, + this.lastExecution()?.id, + ) + .pipe( + finalize(() => { + this._submitOperationEnded(); + }), + ) + .subscribe({ + next: () => { + this.cancelDataJobDisabled = true; + this.toastService.show({ + type: VmwToastType.INFO, + title: `Data job execution cancellation completed`, + description: `Data job "${this.jobName}" successfully canceled`, + }); + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + title: `Data job cancellation failed`, + }, + ); + }, + }); + } + + /** + * ** Show confirmation dialog for Job execution cancellation. + */ + cancelExecution() { + this.cancelNowOptions.title = `Cancel ${this.lastExecution()?.id} now?`; + this.cancelNowOptions.message = `Execution ${this.lastExecution()?.id} will be canceled.`; + this.cancelNowOptions.infoText = `Confirming will result in immediate data job execution cancellation.`; + this.cancelNowOptions.opened = true; + } + + lastExecution(): DataJobExecution { + return this.jobExecutions[this.jobExecutions.length - 1]; + } + + isJobWithRunningStatus(): boolean { + return this.lastExecution().status === "RUNNING"; + } + + /** + * @inheritDoc + */ + onModelInit(): void { + this.routerService + .getState() + .pipe(take(1)) + .subscribe((state) => this._initialize(state)); + } + + /** + * @inheritDoc + */ + onModelError( + model: ComponentModel, + _task: string, + newErrorRecords: ErrorRecord[], + ) { + newErrorRecords.forEach((errorRecord) => { + const error = ErrorUtil.extractError(errorRecord.error); + + this.errorHandlerService.processError(error); + }); + } + + private _initialize(state: RouteState): void { + const teamParamKey = state.getData("teamParamKey"); + this.teamName = state.getParam(teamParamKey); + + if ( + CollectionsUtil.isNil(teamParamKey) || + CollectionsUtil.isNil(this.teamName) + ) { + this._subscribeForImplicitTeam(); } - private _subscribeForImplicitTeam(): void { - this.dataJobsService - .getNotifiedForTeamImplicitly() - .pipe(take(1)) - .subscribe((teamName) => (this.teamName = teamName)); - } + const jobParamKey = state.getData("jobParamKey"); + this.jobName = state.getParam(jobParamKey); - private _subscribeForTeamChange(state: RouteState): void { - const shouldActivateListener = !!state.getData('activateListenerForTeamChange'); + this.isJobEditable = !!state.getData("editable"); - if (shouldActivateListener && this.dataPipelinesModuleConfig?.manageConfig?.selectedTeamNameObservable) { - this.subscriptions.push( - this.dataPipelinesModuleConfig.manageConfig.selectedTeamNameObservable.subscribe((newTeam) => { - if (this.teamName !== newTeam) { - this.teamName = newTeam; + this.queryParams = state.queryParams; - this.doNavigateBack(); - } - }) - ); - } - } + this.isDownloadJobKeyAllowed = + this.dataPipelinesModuleConfig.manageConfig?.allowKeyTabDownloads && + this.isJobEditable; - private _subscribeForExecutionsChange(): void { - this.subscriptions.push( - this.dataJobsService.getNotifiedForJobExecutions().subscribe((executions) => { - this.jobExecutions = [...executions]; - }) - ); - } + this._subscribeForTeamChange(state); + this._subscribeForExecutionsChange(); + this._subscribeForExecutionIdChange(); + this._loadJobDetails(); + this._loadJobExecutions(); + } - private _subscribeForExecutionIdChange(): void { - const scheduleLastExecutionPolling = new Subject(); + private _subscribeForImplicitTeam(): void { + this.dataJobsService + .getNotifiedForTeamImplicitly() + .pipe(take(1)) + .subscribe((teamName) => (this.teamName = teamName)); + } - this.subscriptions.push( - scheduleLastExecutionPolling - .pipe( - switchMap((id) => - interval(5000).pipe( - switchMap(() => - this.dataJobsApiService.getJobExecution(this.teamName, this.jobName, id).pipe( - map((execution) => { - return { - execution, - error: null as Error - }; - }), - catchError((error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error)); - - return of({ - execution: null as DataJobExecutionDetails, - error: error as Error - }); - }) - ) - ), - tap((data) => this._replaceRunningExecutionAndNotify(data.execution)), - takeWhile((data) => { - if (data.error instanceof HttpErrorResponse) { - if (data.error.status === 404 || data.error.status >= 500) { - this.isDataJobRunning = false; - - return false; - } - } - - const isRunning = - CollectionsUtil.isNil(data.execution) || DataJobUtil.isJobRunningPredicate(data.execution); - - if (!isRunning) { - this.isDataJobRunning = false; - } - return isRunning; - }) - ) - ) - ) - .subscribe() - ); + private _subscribeForTeamChange(state: RouteState): void { + const shouldActivateListener = !!state.getData( + "activateListenerForTeamChange", + ); - this.subscriptions.push( - this.dataJobsService - .getNotifiedForRunningJobExecutionId() - .pipe( - concatMap((executionId: string) => - this.dataJobsApiService.getJobExecution(this.teamName, this.jobName, executionId).pipe( - map((executionDetails) => [executionId, executionDetails]), - catchError((error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error)); - - return of([executionId]); - }) - ) - ) - ) - .subscribe(([executionId, executionDetails]: [string, DataJobExecutionDetails]) => { - this.isDataJobRunning = true; - this.cancelDataJobDisabled = false; - this._replaceRunningExecutionAndNotify(executionDetails); - scheduleLastExecutionPolling.next(executionId); - }) - ); + if ( + shouldActivateListener && + this.dataPipelinesModuleConfig?.manageConfig?.selectedTeamNameObservable + ) { + this.subscriptions.push( + this.dataPipelinesModuleConfig.manageConfig.selectedTeamNameObservable.subscribe( + (newTeam) => { + if (this.teamName !== newTeam) { + this.teamName = newTeam; + + this.doNavigateBack(); + } + }, + ), + ); } - - private _loadJobDetails(): void { - this.subscriptions.push( - this.dataJobsApiService.getJobDetails(this.teamName, this.jobName).subscribe({ - error: (error: unknown) => { - if (error instanceof HttpErrorResponse) { - if (error.status === 404) { - this._showMessageJobNotExist(); - this.doNavigateBack(); - } - - console.error('Error loading jobDetails', error); - } + } + + private _subscribeForExecutionsChange(): void { + this.subscriptions.push( + this.dataJobsService + .getNotifiedForJobExecutions() + .subscribe((executions) => { + this.jobExecutions = [...executions]; + }), + ); + } + + private _subscribeForExecutionIdChange(): void { + const scheduleLastExecutionPolling = new Subject(); + + this.subscriptions.push( + scheduleLastExecutionPolling + .pipe( + switchMap((id) => + interval(5000).pipe( + switchMap(() => + this.dataJobsApiService + .getJobExecution(this.teamName, this.jobName, id) + .pipe( + map((execution) => { + return { + execution, + error: null as Error, + }; + }), + catchError((error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + ); + + return of({ + execution: null as DataJobExecutionDetails, + error: error as Error, + }); + }), + ), + ), + tap((data) => + this._replaceRunningExecutionAndNotify(data.execution), + ), + takeWhile((data) => { + if (data.error instanceof HttpErrorResponse) { + if (data.error.status === 404 || data.error.status >= 500) { + this.isDataJobRunning = false; + + return false; + } } - }) - ); - this.subscriptions.push( - this.dataJobsApiService.getJob(this.teamName, this.jobName).subscribe({ - next: (job) => { - if (CollectionsUtil.isDefined(job)) { - this.isJobAvailable = true; - - this.jobDeployments = job.deployments; - this.isExecuteJobAllowed = ExtractJobStatusPipe.transform(this.jobDeployments) !== DataJobStatus.NOT_DEPLOYED; - - return; - } - this._showMessageJobNotExist(); - this.doNavigateBack(); - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - title: `Loading Data job "${this.jobName}" failed` - }); - } - }) - ); - } + const isRunning = + CollectionsUtil.isNil(data.execution) || + DataJobUtil.isJobRunningPredicate(data.execution); - private _loadJobExecutions(): void { - this.subscriptions.push( + if (!isRunning) { + this.isDataJobRunning = false; + } + return isRunning; + }), + ), + ), + ) + .subscribe(), + ); + + this.subscriptions.push( + this.dataJobsService + .getNotifiedForRunningJobExecutionId() + .pipe( + concatMap((executionId: string) => this.dataJobsApiService - .getJobExecutions(this.teamName, this.jobName, true, null, { - property: 'startTime', - direction: ASC - }) - .subscribe({ - next: (value) => { - if (value?.content) { - this.dataJobsService.notifyForJobExecutions([...value.content]); - - // eslint-disable-next-line @typescript-eslint/unbound-method - const runningExecution = value.content.find(DataJobUtil.isJobRunningPredicate); - if (runningExecution) { - this.dataJobsService.notifyForRunningJobExecutionId(runningExecution.id); - } - } - - this.areJobExecutionsLoaded = true; - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error)); - } - }) - ); - } + .getJobExecution(this.teamName, this.jobName, executionId) + .pipe( + map((executionDetails) => [executionId, executionDetails]), + catchError((error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + ); + + return of([executionId]); + }), + ), + ), + ) + .subscribe( + ([executionId, executionDetails]: [ + string, + DataJobExecutionDetails, + ]) => { + this.isDataJobRunning = true; + this.cancelDataJobDisabled = false; + this._replaceRunningExecutionAndNotify(executionDetails); + scheduleLastExecutionPolling.next(executionId); + }, + ), + ); + } + + private _loadJobDetails(): void { + this.subscriptions.push( + this.dataJobsApiService + .getJobDetails(this.teamName, this.jobName) + .subscribe({ + error: (error: unknown) => { + if (error instanceof HttpErrorResponse) { + if (error.status === 404) { + this._showMessageJobNotExist(); + this.doNavigateBack(); + } + + console.error("Error loading jobDetails", error); + } + }, + }), + ); + this.subscriptions.push( + this.dataJobsApiService.getJob(this.teamName, this.jobName).subscribe({ + next: (job) => { + if (CollectionsUtil.isDefined(job)) { + this.isJobAvailable = true; + + this.jobDeployments = job.deployments; + this.isExecuteJobAllowed = + ExtractJobStatusPipe.transform(this.jobDeployments) !== + DataJobStatus.NOT_DEPLOYED; - private _replaceRunningExecutionAndNotify(executionDetails: DataJobExecutionDetails): void { - if (CollectionsUtil.isNil(executionDetails)) { return; - } - - const convertedExecution = DataJobUtil.convertFromExecutionDetailsToExecutionState(executionDetails); - const foundIndex = this.jobExecutions.findIndex((ex) => ex.id === convertedExecution.id); + } + + this._showMessageJobNotExist(); + this.doNavigateBack(); + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + title: `Loading Data job "${this.jobName}" failed`, + }, + ); + }, + }), + ); + } + + private _loadJobExecutions(): void { + this.subscriptions.push( + this.dataJobsApiService + .getJobExecutions(this.teamName, this.jobName, true, null, { + property: "startTime", + direction: ASC, + }) + .subscribe({ + next: (value) => { + if (value?.content) { + this.dataJobsService.notifyForJobExecutions([...value.content]); + + // eslint-disable-next-line @typescript-eslint/unbound-method + const runningExecution = value.content.find( + DataJobUtil.isJobRunningPredicate, + ); + if (runningExecution) { + this.dataJobsService.notifyForRunningJobExecutionId( + runningExecution.id, + ); + } + } + + this.areJobExecutionsLoaded = true; + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + ); + }, + }), + ); + } + + private _replaceRunningExecutionAndNotify( + executionDetails: DataJobExecutionDetails, + ): void { + if (CollectionsUtil.isNil(executionDetails)) { + return; + } - if (foundIndex !== -1) { - this.jobExecutions.splice(foundIndex, 1, convertedExecution); - } else { - this.jobExecutions.push(convertedExecution); - } + const convertedExecution = + DataJobUtil.convertFromExecutionDetailsToExecutionState(executionDetails); + const foundIndex = this.jobExecutions.findIndex( + (ex) => ex.id === convertedExecution.id, + ); - this.dataJobsService.notifyForJobExecutions(this.jobExecutions); + if (foundIndex !== -1) { + this.jobExecutions.splice(foundIndex, 1, convertedExecution); + } else { + this.jobExecutions.push(convertedExecution); } - private _submitOperationStarted(type: TypeButtonState): void { - switch (type) { - case TypeButtonState.DELETE: - this.deleteButtonsState = ClrLoadingState.LOADING; - break; - case TypeButtonState.DOWNLOAD: - this.downloadButtonsState = ClrLoadingState.LOADING; - break; - case TypeButtonState.EXECUTE: - this.executeButtonsState = ClrLoadingState.LOADING; - break; - case TypeButtonState.STOP: - this.stopButtonsState = ClrLoadingState.LOADING; - break; - } - - this.loadingInProgress = true; + this.dataJobsService.notifyForJobExecutions(this.jobExecutions); + } + + private _submitOperationStarted(type: TypeButtonState): void { + switch (type) { + case TypeButtonState.DELETE: + this.deleteButtonsState = ClrLoadingState.LOADING; + break; + case TypeButtonState.DOWNLOAD: + this.downloadButtonsState = ClrLoadingState.LOADING; + break; + case TypeButtonState.EXECUTE: + this.executeButtonsState = ClrLoadingState.LOADING; + break; + case TypeButtonState.STOP: + this.stopButtonsState = ClrLoadingState.LOADING; + break; } - private _submitOperationEnded(): void { - this.deleteButtonsState = ClrLoadingState.DEFAULT; - this.downloadButtonsState = ClrLoadingState.DEFAULT; - this.executeButtonsState = ClrLoadingState.DEFAULT; - this.stopButtonsState = ClrLoadingState.DEFAULT; + this.loadingInProgress = true; + } - this.loadingInProgress = false; - } + private _submitOperationEnded(): void { + this.deleteButtonsState = ClrLoadingState.DEFAULT; + this.downloadButtonsState = ClrLoadingState.DEFAULT; + this.executeButtonsState = ClrLoadingState.DEFAULT; + this.stopButtonsState = ClrLoadingState.DEFAULT; - private _extractJobDeployment(): DataJobDeployment { - if (!this.jobDeployments) { - return null; - } + this.loadingInProgress = false; + } - return this.jobDeployments[this.jobDeployments.length - 1]; + private _extractJobDeployment(): DataJobDeployment { + if (!this.jobDeployments) { + return null; } - private _showMessageJobNotExist(): void { - if (!this._nonExistingJobMsgShowed) { - this._nonExistingJobMsgShowed = true; + return this.jobDeployments[this.jobDeployments.length - 1]; + } + + private _showMessageJobNotExist(): void { + if (!this._nonExistingJobMsgShowed) { + this._nonExistingJobMsgShowed = true; - this.toastService.show({ - type: VmwToastType.FAILURE, - title: `Job "${this.jobName}" doesn't exist`, - description: `Data Job "${this.jobName}" for Team "${this.teamName}" doesn't exist, will load Data Jobs list` - }); - } + this.toastService.show({ + type: VmwToastType.FAILURE, + title: `Job "${this.jobName}" doesn't exist`, + description: `Data Job "${this.jobName}" for Team "${this.teamName}" doesn't exist, will load Data Jobs list`, + }); } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/index.ts index fd57fbf3b9..f391e03dd9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-page.component'; +export * from "./data-job-page.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.html index 6020009156..a10891dda4 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.html @@ -6,877 +6,735 @@
-
- -
-
-
- -
- Status -
+
+ +
+
+ + +
Status
-
- -
+
+ +
-
- - - - - - - - - - - -
-
+
+ + + + + + + + + + + +
+ - -
- Description -
+ +
Description
-
- {{ description.value }}
- -
+
+ {{ description.value }}
+ +
- {{ description.value | words : - descriptionWordsBeforeTruncate }} - - -
+ {{ + description.value | words: descriptionWordsBeforeTruncate + }} + + +
-
- - - - -
-
+
+ + + + +
+
- -
- Python version -
+ +
+ Python version +
-
- {{ jobPythonVersion.value }} -
-
+
+ {{ jobPythonVersion.value }} +
+
- -
- Owner team -
- -
+ +
Owner team
+ +
- -
- Schedule (in UTC) -
-
- {{ jobDetails?.config?.schedule?.schedule_cron | - formatSchedule : "Not scheduled" }} + +
+ Schedule (in UTC) +
+
+ {{ + jobDetails?.config?.schedule?.schedule_cron + | formatSchedule: "Not scheduled" + }} - - - Cron expression
- {{ jobDetails?.config?.schedule - ?.schedule_cron }} -
-
- Next 5 executions -
    -
  1. - {{ jobDetails?.config?.schedule - ?.schedule_cron | parseNextRun : - times | date : "MMM d, y, hh:mm - a" : "UTC" }} UTC -
  2. -
-
{{ cronError }}
-
-
-
+ + + Cron expression
+ {{ jobDetails?.config?.schedule?.schedule_cron }} +
+
+ Next 5 executions +
    +
  1. + {{ + jobDetails?.config?.schedule?.schedule_cron + | parseNextRun: times + | date + : "MMM d, y, hh:mm + a" + : "UTC" + }} + UTC +
  2. +
+
{{ cronError }}
+
+
+
-
- - - - -
-
+
+ + + + +
+ - -
- Change history -
- -
+ +
+ Change history +
+ +
- -
- Source location -
-
-
-
- The data job is not deployed -
- -
-
-
- -
-
-
+
+ Source location +
+
+
+
+ The data job is not deployed +
+
+ - -
- Notifications - - - Notifications are used to inform the - users about a specific activity. To - configure notifications, edit data job's - config.ini file and redeploy the data - job. - - -
- -
-
- On Job Deployed -
-
- -
-
- {{ contacts - }} -
-
-
-
- - - -
    -
  • - {{ contacts - }} -
  • -
-
-
-
- - Not configured - -
-
-
-
- -
-
- -
-
- {{ contacts - }} -
-
-
-
- - - -
    -
  • - {{ contacts - }} -
  • -
-
-
-
- - Not configured - -
-
-
-
- -
-
- -
-
- {{ contacts - }} -
-
-
-
- - - -
    -
  • - {{ contacts - }} -
  • -
-
-
-
- - Not configured - -
-
-
-
- -
-
- -
-
- {{ contacts - }} -
-
-
-
- - - -
    -
  • - {{ contacts - }} -
  • -
-
-
-
- - Not configured - -
-
-
-
- -
- - - - - - - - - - - - - - - - - -
-
- + {{ jobState?.config?.sourceUrl }} +
+
-
+
+ + +
+
+
+ +
+ Notifications + + + Notifications are used to inform the users about a specific + activity. To configure notifications, edit data job's + config.ini file and redeploy the data job. + + +
-
-
-
- Last 5 Executions - -
+
+
+ On Job Deployed +
+ +
+
+ {{ contacts }} +
+
+
+
+ + + +
    +
  • + {{ contacts }} +
  • +
+
+
+
+ + Not configured + +
+
+
+
+ +
+
-
- - We couldn't find any executions, but you can always - schedule one! - + +
+
+ {{ contacts }} +
+
+
+
+ + + +
    +
  • + {{ contacts }} +
  • +
+
+
+
+ + Not configured +
- + +
+
+ +
+
+ {{ contacts }} +
+
+
+
+ class="data-pipelines-job__contacts-list-signpost-container" + > + + + +
    +
  • + {{ contacts }} +
  • +
+
+
+
+ + Not configured + +
+
-
-
Loading executions...
+
+ +
+
+ +
+
+ {{ contacts }} +
+
+
+
+ + + +
    +
  • + {{ contacts }} +
  • +
+
+
+
+ + Not configured + +
+
-
- +
- - + + + + + + + + + + + + + + + + + +
+ + +
+
+ +
+
+
+ Last 5 Executions +
+ + + +
+
+ + We couldn't find any executions, but you can always schedule one! + +
+ +
+
+
Loading executions...
+
+
+
+ + + + + +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.scss index 4f622bee31..e5ae886ef8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.scss @@ -4,144 +4,144 @@ */ @mixin fill-parent-content() { - display: flex; - flex-direction: column; - flex: 1 1 auto; + display: flex; + flex-direction: column; + flex: 1 1 auto; } hr { - border: 0; - height: 1px; - background: #8f9ba3; + border: 0; + height: 1px; + background: #8f9ba3; } .label-link { - cursor: pointer; + cursor: pointer; } .label-link-suppress-decoration { - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; + } } ::ng-deep .edit-column-modal { - .modal-header { - padding: 0 0 0 0 !important; - } + .modal-header { + padding: 0 0 0 0 !important; + } } ::ng-deep .clr-timeline-horizontal { - padding-top: 0 !important; + padding-top: 0 !important; } .status-icon-enabled { - color: hsl(93, 67%, 38%); + color: hsl(93, 67%, 38%); } .clr-col-6 { - border-right: 1px solid #999999; + border-right: 1px solid #999999; } .placeholder-error-label { - color: gray; + color: gray; } .grid-column-details { - display: grid; - height: 100%; - grid-template: 'a a a b b b' auto 'c c c c c c' auto 'd d d d d d' auto 'f f f f f f' auto/ 1fr 1fr 1fr 1fr 1fr 1fr; + display: grid; + height: 100%; + grid-template: "a a a b b b" auto "c c c c c c" auto "d d d d d d" auto "f f f f f f" auto/ 1fr 1fr 1fr 1fr 1fr 1fr; - .grid-area-a { - grid-area: a; - } + .grid-area-a { + grid-area: a; + } - .grid-area-b { - grid-area: b; - } + .grid-area-b { + grid-area: b; + } - .grid-area-c { - grid-area: c; - } + .grid-area-c { + grid-area: c; + } - .grid-area-d { - grid-area: d; - } + .grid-area-d { + grid-area: d; + } - .grid-area-e { - grid-area: e; - } + .grid-area-e { + grid-area: e; + } - .grid-area-f { - grid-area: f; - } + .grid-area-f { + grid-area: f; + } } -.clr-timeline-step clr-icon[shape='success-standard'] { - color: var(--clr-timeline-success-step-color, #5eb715); +.clr-timeline-step clr-icon[shape="success-standard"] { + color: var(--clr-timeline-success-step-color, #5eb715); } -.clr-timeline-step clr-icon[shape='error-standard'] { - color: var(--clr-timeline-error-step-color, #c21d00); +.clr-timeline-step clr-icon[shape="error-standard"] { + color: var(--clr-timeline-error-step-color, #c21d00); } .clr-timeline-step clr-icon { - height: 1.8rem; - width: 1.8rem; - min-height: 1.8rem; - min-width: 1.8rem; + height: 1.8rem; + width: 1.8rem; + min-height: 1.8rem; + min-width: 1.8rem; } .btn-show-more { - padding: 0 !important; + padding: 0 !important; } :host { - @include fill-parent-content(); + @include fill-parent-content(); } .data-pipelines-job__details-page { - @include fill-parent-content(); + @include fill-parent-content(); - .data-pipelines-job__details-body { - @include fill-parent-content(); + .data-pipelines-job__details-body { + @include fill-parent-content(); - margin-bottom: 1rem; + margin-bottom: 1rem; - .form-section-readonly { - word-break: break-word; - } + .form-section-readonly { + word-break: break-word; + } - .data-pipelines-job__section--border-none { - border: none; - } + .data-pipelines-job__section--border-none { + border: none; + } - .data-pipelines-job__contacts-container { - display: flex; - align-items: center; + .data-pipelines-job__contacts-container { + display: flex; + align-items: center; - .data-pipelines-job__contacts-list-container { - display: inline-flex; - align-items: center; + .data-pipelines-job__contacts-list-container { + display: inline-flex; + align-items: center; - > div { - display: inline-flex; - margin-right: 0.3rem; + > div { + display: inline-flex; + margin-right: 0.3rem; - &:last-child { - margin-right: 0; - } - } - } + &:last-child { + margin-right: 0; + } + } + } - .data-pipelines-job__contacts-list-signpost-container { - display: inline-flex; - align-items: center; + .data-pipelines-job__contacts-list-signpost-container { + display: inline-flex; + align-items: center; - ul { - list-style-type: none; - } - } + ul { + list-style-type: none; } + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.spec.ts index a4f1aab1db..5bb6bc35f0 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.spec.ts @@ -3,337 +3,397 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { RouterTestingModule } from "@angular/router/testing"; -import { BehaviorSubject, of, Subject } from 'rxjs'; +import { BehaviorSubject, of, Subject } from "rxjs"; import { - ComponentModel, - ComponentService, - ComponentStateImpl, - ErrorHandlerService, - FORM_STATE, - generateErrorCodes, - NavigationService, - RouterService, - RouterState, - RouteState, - ToastService, - UrlOpenerService, - VdkFormState -} from '@versatiledatakit/shared'; - -import { DataJobsApiService, DataJobsService } from '../../../../services'; - -import { ExtractContactsPipe, ExtractJobStatusPipe, FormatSchedulePipe } from '../../../../shared/pipes'; + ComponentModel, + ComponentService, + ComponentStateImpl, + ErrorHandlerService, + FORM_STATE, + generateErrorCodes, + NavigationService, + RouterService, + RouterState, + RouteState, + ToastService, + UrlOpenerService, + VdkFormState, +} from "@versatiledatakit/shared"; + +import { DataJobsApiService, DataJobsService } from "../../../../services"; import { - DATA_PIPELINES_CONFIGS, - DataJob, - DataJobDeploymentDetails, - DataJobDeploymentStatus, - DataJobDetails, - DataJobExecution, - DataJobExecutionsPage, - DataJobExecutionStatus, - DataJobExecutionType -} from '../../../../model'; + ExtractContactsPipe, + ExtractJobStatusPipe, + FormatSchedulePipe, +} from "../../../../shared/pipes"; -import { LOAD_JOB_ERROR_CODES } from '../../../../state/error-codes'; +import { + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobDeploymentDetails, + DataJobDeploymentStatus, + DataJobDetails, + DataJobExecution, + DataJobExecutionsPage, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../../../model"; + +import { LOAD_JOB_ERROR_CODES } from "../../../../state/error-codes"; -import { TASK_LOAD_JOB_DETAILS, TASK_LOAD_JOB_STATE } from '../../../../state/tasks'; +import { + TASK_LOAD_JOB_DETAILS, + TASK_LOAD_JOB_STATE, +} from "../../../../state/tasks"; -import { DataJobDetailsPageComponent } from './data-job-details-page.component'; +import { DataJobDetailsPageComponent } from "./data-job-details-page.component"; const TEST_JOB_EXECUTION = { - id: 'id002', - jobName: 'job002', - status: DataJobExecutionStatus.SUBMITTED, - startTime: new Date().toISOString(), - startedBy: 'aUserov', - endTime: new Date().toISOString(), - type: DataJobExecutionType.MANUAL, - opId: 'op002', - message: 'message001', - deployment: { - id: 'id002', - enabled: true, - jobVersion: '002', - mode: 'test_mode', - vdkVersion: '002', - jobPythonVersion: '3.9-secure', - resources: { - memoryLimit: 1000, - memoryRequest: 1000, - cpuLimit: 0.5, - cpuRequest: 0.5 - }, - executions: [], - deployedDate: '2020-11-11T10:10:10Z', - deployedBy: 'pmitev', - status: DataJobDeploymentStatus.SUCCESS - } + id: "id002", + jobName: "job002", + status: DataJobExecutionStatus.SUBMITTED, + startTime: new Date().toISOString(), + startedBy: "aUserov", + endTime: new Date().toISOString(), + type: DataJobExecutionType.MANUAL, + opId: "op002", + message: "message001", + deployment: { + id: "id002", + enabled: true, + jobVersion: "002", + mode: "test_mode", + vdkVersion: "002", + jobPythonVersion: "3.9-secure", + resources: { + memoryLimit: 1000, + memoryRequest: 1000, + cpuLimit: 0.5, + cpuRequest: 0.5, + }, + executions: [], + deployedDate: "2020-11-11T10:10:10Z", + deployedBy: "pmitev", + status: DataJobDeploymentStatus.SUCCESS, + }, } as DataJobExecution; const TEST_JOB_DEPLOYMENT = { - id: 'id001', - enabled: true, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - job_version: '001', - mode: 'test_mode', - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - vdk_version: '001', - python_version: '3.7-secure' + id: "id001", + enabled: true, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + job_version: "001", + mode: "test_mode", + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + vdk_version: "001", + python_version: "3.7-secure", } as DataJobDeploymentDetails; const TEST_JOB_DETAILS = { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - job_name: 'job001', - team: 'taurus', - description: 'description' + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + job_name: "job001", + team: "taurus", + description: "description", }; -describe('DataJobsDetailsModalComponent', () => { - let componentServiceStub: jasmine.SpyObj; - let navigationServiceStub: jasmine.SpyObj; - let activatedRouteStub: ActivatedRoute; - let routerServiceStub: jasmine.SpyObj; - let toastServiceStub: jasmine.SpyObj; - let dataJobsApiServiceStub: jasmine.SpyObj; - let dataJobsServiceStub: jasmine.SpyObj; - let errorHandlerServiceStub: jasmine.SpyObj; - let urlOpenerServiceStub: jasmine.SpyObj; - - let componentModelStub: ComponentModel; - let component: DataJobDetailsPageComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - componentServiceStub = jasmine.createSpyObj('componentService', ['init', 'getModel', 'idle', 'update']); - navigationServiceStub = jasmine.createSpyObj('navigationService', ['navigateTo', 'navigateBack']); - activatedRouteStub = { snapshot: null } as any; - routerServiceStub = jasmine.createSpyObj('routerService', ['getState']); - toastServiceStub = jasmine.createSpyObj('toastService', ['show']); - dataJobsApiServiceStub = jasmine.createSpyObj('dataJobsApiService', [ - 'getJobDetails', - 'getJobExecutions', - 'getJobDeployments', - 'downloadFile', - 'updateDataJobStatus', - 'updateDataJob', - 'executeDataJob', - 'removeJob', - 'getJob' - ]); - dataJobsServiceStub = jasmine.createSpyObj('dataJobsService', [ - 'loadJobs', - 'loadJob', - 'notifyForRunningJobExecutionId', - 'notifyForJobExecutions', - 'notifyForTeamImplicitly', - 'getNotifiedForRunningJobExecutionId', - 'getNotifiedForJobExecutions', - 'getNotifiedForTeamImplicitly' - ]); - errorHandlerServiceStub = jasmine.createSpyObj('errorHandlerService', ['processError', 'handleError']); - urlOpenerServiceStub = jasmine.createSpyObj('urlOpenerServiceStub', ['open']); - - dataJobsApiServiceStub.getJobDetails.and.returnValue(new BehaviorSubject(TEST_JOB_DETAILS).asObservable()); - dataJobsApiServiceStub.getJobExecutions.and.returnValue( - new BehaviorSubject({ - content: [TEST_JOB_EXECUTION], - totalItems: 1, - totalPages: 1 - }).asObservable() - ); - dataJobsApiServiceStub.getJobDeployments.and.returnValue( - new BehaviorSubject([TEST_JOB_DEPLOYMENT]).asObservable() - ); - dataJobsApiServiceStub.downloadFile.and.returnValue(new BehaviorSubject({} as never).asObservable()); - dataJobsApiServiceStub.updateDataJobStatus.and.returnValue( - new BehaviorSubject<{ enabled: boolean }>({ - enabled: true - }).asObservable() - ); - dataJobsApiServiceStub.updateDataJob.and.returnValue(new BehaviorSubject({}).asObservable()); - dataJobsApiServiceStub.executeDataJob.and.returnValue(new BehaviorSubject(undefined).asObservable()); - dataJobsApiServiceStub.removeJob.and.returnValue(new BehaviorSubject(TEST_JOB_DETAILS).asObservable()); - dataJobsApiServiceStub.getJob.and.returnValue(of({ data: { content: [TEST_JOB_DETAILS] } } as DataJob)); - - dataJobsServiceStub.getNotifiedForJobExecutions.and.returnValue(new Subject()); - dataJobsServiceStub.getNotifiedForTeamImplicitly.and.returnValue(new BehaviorSubject(TEST_JOB_DETAILS.team)); - - componentModelStub = ComponentModel.of(ComponentStateImpl.of({}), RouterState.of(RouteState.empty(), 1)); - routerServiceStub.getState.and.returnValue(new Subject()); - componentServiceStub.init.and.returnValue(of(componentModelStub)); - componentServiceStub.getModel.and.returnValue(of(componentModelStub)); - - navigationServiceStub.navigateBack.and.returnValue(Promise.resolve(true)); - - generateErrorCodes(dataJobsApiServiceStub, ['getJob', 'getJobDetails']); - - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE] = dataJobsApiServiceStub.errorCodes.getJob; - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS] = dataJobsApiServiceStub.errorCodes.getJobDetails; - - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DataJobDetailsPageComponent, FormatSchedulePipe, ExtractJobStatusPipe, ExtractContactsPipe], - imports: [RouterTestingModule], - providers: [ - FormBuilder, - { provide: RouterService, useValue: routerServiceStub }, - { provide: ComponentService, useValue: componentServiceStub }, - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, - { - provide: DataJobsApiService, - useValue: dataJobsApiServiceStub - }, - { provide: ToastService, useValue: toastServiceStub }, - { provide: DataJobsService, useValue: dataJobsServiceStub }, - { - provide: ErrorHandlerService, - useValue: errorHandlerServiceStub - }, - { provide: UrlOpenerService, useValue: urlOpenerServiceStub }, - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => ({ - defaultOwnerTeamName: 'all', - manageConfig: { - allowKeyTabDownloads: true, - allowExecuteNow: true - } - }) - } - ] - }); - - fixture = TestBed.createComponent(DataJobDetailsPageComponent); - component = fixture.componentInstance; - component.jobDetails = TEST_JOB_DETAILS; - component.jobExecutions = [TEST_JOB_EXECUTION]; - component.model = componentModelStub; +describe("DataJobsDetailsModalComponent", () => { + let componentServiceStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let activatedRouteStub: ActivatedRoute; + let routerServiceStub: jasmine.SpyObj; + let toastServiceStub: jasmine.SpyObj; + let dataJobsApiServiceStub: jasmine.SpyObj; + let dataJobsServiceStub: jasmine.SpyObj; + let errorHandlerServiceStub: jasmine.SpyObj; + let urlOpenerServiceStub: jasmine.SpyObj; + + let componentModelStub: ComponentModel; + let component: DataJobDetailsPageComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["init", "getModel", "idle", "update"], + ); + navigationServiceStub = jasmine.createSpyObj( + "navigationService", + ["navigateTo", "navigateBack"], + ); + activatedRouteStub = { snapshot: null } as any; + routerServiceStub = jasmine.createSpyObj("routerService", [ + "getState", + ]); + toastServiceStub = jasmine.createSpyObj("toastService", [ + "show", + ]); + dataJobsApiServiceStub = jasmine.createSpyObj( + "dataJobsApiService", + [ + "getJobDetails", + "getJobExecutions", + "getJobDeployments", + "downloadFile", + "updateDataJobStatus", + "updateDataJob", + "executeDataJob", + "removeJob", + "getJob", + ], + ); + dataJobsServiceStub = jasmine.createSpyObj( + "dataJobsService", + [ + "loadJobs", + "loadJob", + "notifyForRunningJobExecutionId", + "notifyForJobExecutions", + "notifyForTeamImplicitly", + "getNotifiedForRunningJobExecutionId", + "getNotifiedForJobExecutions", + "getNotifiedForTeamImplicitly", + ], + ); + errorHandlerServiceStub = jasmine.createSpyObj( + "errorHandlerService", + ["processError", "handleError"], + ); + urlOpenerServiceStub = jasmine.createSpyObj( + "urlOpenerServiceStub", + ["open"], + ); + + dataJobsApiServiceStub.getJobDetails.and.returnValue( + new BehaviorSubject(TEST_JOB_DETAILS).asObservable(), + ); + dataJobsApiServiceStub.getJobExecutions.and.returnValue( + new BehaviorSubject({ + content: [TEST_JOB_EXECUTION], + totalItems: 1, + totalPages: 1, + }).asObservable(), + ); + dataJobsApiServiceStub.getJobDeployments.and.returnValue( + new BehaviorSubject([ + TEST_JOB_DEPLOYMENT, + ]).asObservable(), + ); + dataJobsApiServiceStub.downloadFile.and.returnValue( + new BehaviorSubject({} as never).asObservable(), + ); + dataJobsApiServiceStub.updateDataJobStatus.and.returnValue( + new BehaviorSubject<{ enabled: boolean }>({ + enabled: true, + }).asObservable(), + ); + dataJobsApiServiceStub.updateDataJob.and.returnValue( + new BehaviorSubject({}).asObservable(), + ); + dataJobsApiServiceStub.executeDataJob.and.returnValue( + new BehaviorSubject(undefined).asObservable(), + ); + dataJobsApiServiceStub.removeJob.and.returnValue( + new BehaviorSubject(TEST_JOB_DETAILS).asObservable(), + ); + dataJobsApiServiceStub.getJob.and.returnValue( + of({ data: { content: [TEST_JOB_DETAILS] } } as DataJob), + ); + + dataJobsServiceStub.getNotifiedForJobExecutions.and.returnValue( + new Subject(), + ); + dataJobsServiceStub.getNotifiedForTeamImplicitly.and.returnValue( + new BehaviorSubject(TEST_JOB_DETAILS.team), + ); + + componentModelStub = ComponentModel.of( + ComponentStateImpl.of({}), + RouterState.of(RouteState.empty(), 1), + ); + routerServiceStub.getState.and.returnValue(new Subject()); + componentServiceStub.init.and.returnValue(of(componentModelStub)); + componentServiceStub.getModel.and.returnValue(of(componentModelStub)); + + navigationServiceStub.navigateBack.and.returnValue(Promise.resolve(true)); + + generateErrorCodes(dataJobsApiServiceStub, [ + "getJob", + "getJobDetails", + ]); + + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE] = + dataJobsApiServiceStub.errorCodes.getJob; + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS] = + dataJobsApiServiceStub.errorCodes.getJobDetails; + + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [ + DataJobDetailsPageComponent, + FormatSchedulePipe, + ExtractJobStatusPipe, + ExtractContactsPipe, + ], + imports: [RouterTestingModule], + providers: [ + FormBuilder, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ComponentService, useValue: componentServiceStub }, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { + provide: DataJobsApiService, + useValue: dataJobsApiServiceStub, + }, + { provide: ToastService, useValue: toastServiceStub }, + { provide: DataJobsService, useValue: dataJobsServiceStub }, + { + provide: ErrorHandlerService, + useValue: errorHandlerServiceStub, + }, + { provide: UrlOpenerService, useValue: urlOpenerServiceStub }, + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => ({ + defaultOwnerTeamName: "all", + manageConfig: { + allowKeyTabDownloads: true, + allowExecuteNow: true, + }, + }), + }, + ], }); - it('can load instance', () => { - expect(component).toBeTruthy(); - }); + fixture = TestBed.createComponent(DataJobDetailsPageComponent); + component = fixture.componentInstance; + component.jobDetails = TEST_JOB_DETAILS; + component.jobExecutions = [TEST_JOB_EXECUTION]; + component.model = componentModelStub; + }); - it(`readOnly has default value`, () => { - expect(component.isJobEditable).toBeFalse(); - }); + it("can load instance", () => { + expect(component).toBeTruthy(); + }); - it(`loadingExecutions has default value`, () => { - expect(component.loadingExecutions).toBeTrue(); - }); + it(`readOnly has default value`, () => { + expect(component.isJobEditable).toBeFalse(); + }); - it(`canEditSection has default value`, () => { - expect(component.canEditSection).toBeTrue(); - }); + it(`loadingExecutions has default value`, () => { + expect(component.loadingExecutions).toBeTrue(); + }); - it(`readOnly sets readFormState as formState`, () => { - component.isJobEditable = false; - component.ngOnInit(); + it(`canEditSection has default value`, () => { + expect(component.canEditSection).toBeTrue(); + }); - expect(component.formState).toEqual(component.readFormState); + it(`readOnly sets readFormState as formState`, () => { + component.isJobEditable = false; + component.ngOnInit(); + + expect(component.formState).toEqual(component.readFormState); + }); + + describe("isDescriptionSubmitEnabled", () => { + it("returns false", () => { + expect(component.isDescriptionSubmitEnabled()).toBeFalse(); }); + }); - describe('isDescriptionSubmitEnabled', () => { - it('returns false', () => { - expect(component.isDescriptionSubmitEnabled()).toBeFalse(); - }); + describe("isStatusSubmitEnabled", () => { + it("returns false", () => { + expect(component.isStatusSubmitEnabled()).toBeFalse(); }); + }); - describe('isStatusSubmitEnabled', () => { - it('returns false', () => { - expect(component.isStatusSubmitEnabled()).toBeFalse(); - }); + describe("_resetJobDetails", () => { + it("do not reset valid job", () => { + // @ts-ignore + component._resetJobDetails(); + expect(component.jobDetails).toEqual(TEST_JOB_DETAILS); }); - describe('_resetJobDetails', () => { - it('do not reset valid job', () => { - // @ts-ignore - component._resetJobDetails(); - expect(component.jobDetails).toEqual(TEST_JOB_DETAILS); - }); - - it('do reset invalid job', () => { - component.jobDetails = null; - // @ts-ignore - component._resetJobDetails(); - expect(component.jobDetails).toBeDefined(); - }); + it("do reset invalid job", () => { + component.jobDetails = null; + // @ts-ignore + component._resetJobDetails(); + expect(component.jobDetails).toBeDefined(); }); + }); - describe('sectionStateChange', () => { - it('makes expected calls for FORM_STATE.SUBMIT', () => { - const vMWFormStateStub = {} as VdkFormState; - vMWFormStateStub.state = FORM_STATE.SUBMIT; + describe("sectionStateChange", () => { + it("makes expected calls for FORM_STATE.SUBMIT", () => { + const vMWFormStateStub = {} as VdkFormState; + vMWFormStateStub.state = FORM_STATE.SUBMIT; - spyOn(component, 'submitForm').and.callThrough(); - component.sectionStateChange(vMWFormStateStub); - expect(component.submitForm).toHaveBeenCalled(); - }); + spyOn(component, "submitForm").and.callThrough(); + component.sectionStateChange(vMWFormStateStub); + expect(component.submitForm).toHaveBeenCalled(); }); - - describe('sectionStateChange', () => { - it('makes expected calls for FORM_STATE.CAN_EDIT', () => { - const vMWFormStateStub = {} as VdkFormState; - vMWFormStateStub.state = FORM_STATE.CAN_EDIT; - component.sectionStateChange(vMWFormStateStub); - expect(component.canEditSection).toBeTrue(); - }); + }); + + describe("sectionStateChange", () => { + it("makes expected calls for FORM_STATE.CAN_EDIT", () => { + const vMWFormStateStub = {} as VdkFormState; + vMWFormStateStub.state = FORM_STATE.CAN_EDIT; + component.sectionStateChange(vMWFormStateStub); + expect(component.canEditSection).toBeTrue(); }); + }); - describe('sectionStateChange', () => { - it('makes expected calls for FORM_STATE.EDIT', () => { - const vMWFormStateStub = {} as VdkFormState; - vMWFormStateStub.state = FORM_STATE.EDIT; + describe("sectionStateChange", () => { + it("makes expected calls for FORM_STATE.EDIT", () => { + const vMWFormStateStub = {} as VdkFormState; + vMWFormStateStub.state = FORM_STATE.EDIT; - component.sectionStateChange(vMWFormStateStub); - expect(component.canEditSection).toBeFalse(); - }); + component.sectionStateChange(vMWFormStateStub); + expect(component.canEditSection).toBeFalse(); }); + }); - describe('doSubmit', () => { - it('makes expected calls for emittingSection description', () => { - const vMWFormStateStub = {} as VdkFormState; - vMWFormStateStub.emittingSection = 'description'; + describe("doSubmit", () => { + it("makes expected calls for emittingSection description", () => { + const vMWFormStateStub = {} as VdkFormState; + vMWFormStateStub.emittingSection = "description"; - spyOn(component, 'isDescriptionSubmitEnabled').and.callThrough(); - component.submitForm(vMWFormStateStub); - expect(component.isDescriptionSubmitEnabled).toHaveBeenCalled(); - }); + spyOn(component, "isDescriptionSubmitEnabled").and.callThrough(); + component.submitForm(vMWFormStateStub); + expect(component.isDescriptionSubmitEnabled).toHaveBeenCalled(); + }); - it('makes expected calls for emittingSection status', () => { - const vMWFormStateStub = {} as VdkFormState; - vMWFormStateStub.emittingSection = 'status'; + it("makes expected calls for emittingSection status", () => { + const vMWFormStateStub = {} as VdkFormState; + vMWFormStateStub.emittingSection = "status"; - spyOn(component, 'isStatusSubmitEnabled').and.callThrough(); - component.submitForm(vMWFormStateStub); - expect(component.isStatusSubmitEnabled).toHaveBeenCalled(); - }); + spyOn(component, "isStatusSubmitEnabled").and.callThrough(); + component.submitForm(vMWFormStateStub); + expect(component.isStatusSubmitEnabled).toHaveBeenCalled(); }); + }); - describe('ngOnInit', () => { - it('makes expected calls', () => { - routerServiceStub.getState.and.returnValue(of(RouteState.empty())); - component.ngOnInit(); + describe("ngOnInit", () => { + it("makes expected calls", () => { + routerServiceStub.getState.and.returnValue(of(RouteState.empty())); + component.ngOnInit(); - expect(dataJobsServiceStub.loadJob).toHaveBeenCalled(); - }); + expect(dataJobsServiceStub.loadJob).toHaveBeenCalled(); }); + }); - describe('editOperationEnded', () => { - it('sets expected states', () => { - component.editOperationEnded(); - expect(component.formState.state).toEqual(FORM_STATE.CAN_EDIT); - expect(component.canEditSection).toBeTrue(); - }); + describe("editOperationEnded", () => { + it("sets expected states", () => { + component.editOperationEnded(); + expect(component.formState.state).toEqual(FORM_STATE.CAN_EDIT); + expect(component.canEditSection).toBeTrue(); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.ts index 9fb21dfa13..9ab94ff623 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/data-job-details-page.component.ts @@ -3,584 +3,662 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Inject, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; -import { take } from 'rxjs/operators'; +import { take } from "rxjs/operators"; import { - ASC, - CollectionsUtil, - ComponentModel, - ComponentService, - ErrorHandlerConfig, - ErrorHandlerService, - ErrorRecord, - FORM_STATE, - NavigationService, - OnTaurusModelChange, - OnTaurusModelError, - OnTaurusModelInit, - OnTaurusModelLoad, - RouterService, - RouteState, - TaurusBaseComponent, - ToastService, - UrlOpenerService, - VdkFormState, - VmwToastType -} from '@versatiledatakit/shared'; - -import { ConfirmationModalOptions, DeleteModalOptions, ModalOptions } from '../../../../shared/model'; -import { CronUtil, DataJobUtil, ErrorUtil, StringUtil } from '../../../../shared/utils'; -import { ExtractJobStatusPipe, ParseEpochPipe } from '../../../../shared/pipes'; + ASC, + CollectionsUtil, + ComponentModel, + ComponentService, + ErrorHandlerConfig, + ErrorHandlerService, + ErrorRecord, + FORM_STATE, + NavigationService, + OnTaurusModelChange, + OnTaurusModelError, + OnTaurusModelInit, + OnTaurusModelLoad, + RouterService, + RouteState, + TaurusBaseComponent, + ToastService, + UrlOpenerService, + VdkFormState, + VmwToastType, +} from "@versatiledatakit/shared"; import { - DATA_PIPELINES_CONFIGS, - DataJob, - DataJobDeployment, - DataJobDetails, - DataJobExecutionOrder, - DataJobExecutions, - DataJobStatus, - DataPipelinesConfig, - JOB_DEPLOYMENT_ID_REQ_PARAM, - JOB_DETAILS_DATA_KEY, - JOB_DETAILS_REQ_PARAM, - JOB_EXECUTIONS_DATA_KEY, - JOB_NAME_REQ_PARAM, - JOB_STATE_DATA_KEY, - JOB_STATE_REQ_PARAM, - JOB_STATUS_REQ_PARAM, - ORDER_REQ_PARAM, - TEAM_NAME_REQ_PARAM -} from '../../../../model'; + ConfirmationModalOptions, + DeleteModalOptions, + ModalOptions, +} from "../../../../shared/model"; +import { + CronUtil, + DataJobUtil, + ErrorUtil, + StringUtil, +} from "../../../../shared/utils"; +import { ExtractJobStatusPipe, ParseEpochPipe } from "../../../../shared/pipes"; import { - TASK_LOAD_JOB_DETAILS, - TASK_LOAD_JOB_EXECUTIONS, - TASK_LOAD_JOB_STATE, - TASK_UPDATE_JOB_DESCRIPTION, - TASK_UPDATE_JOB_STATUS -} from '../../../../state/tasks'; + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobDeployment, + DataJobDetails, + DataJobExecutionOrder, + DataJobExecutions, + DataJobStatus, + DataPipelinesConfig, + JOB_DEPLOYMENT_ID_REQ_PARAM, + JOB_DETAILS_DATA_KEY, + JOB_DETAILS_REQ_PARAM, + JOB_EXECUTIONS_DATA_KEY, + JOB_NAME_REQ_PARAM, + JOB_STATE_DATA_KEY, + JOB_STATE_REQ_PARAM, + JOB_STATUS_REQ_PARAM, + ORDER_REQ_PARAM, + TEAM_NAME_REQ_PARAM, +} from "../../../../model"; -import { LOAD_JOB_ERROR_CODES } from '../../../../state/error-codes'; +import { + TASK_LOAD_JOB_DETAILS, + TASK_LOAD_JOB_EXECUTIONS, + TASK_LOAD_JOB_STATE, + TASK_UPDATE_JOB_DESCRIPTION, + TASK_UPDATE_JOB_STATUS, +} from "../../../../state/tasks"; -import { DataJobsApiService, DataJobsService } from '../../../../services'; +import { LOAD_JOB_ERROR_CODES } from "../../../../state/error-codes"; + +import { DataJobsApiService, DataJobsService } from "../../../../services"; @Component({ - selector: 'lib-data-job-details-page', - templateUrl: './data-job-details-page.component.html', - styleUrls: ['./data-job-details-page.component.scss'] + selector: "lib-data-job-details-page", + templateUrl: "./data-job-details-page.component.html", + styleUrls: ["./data-job-details-page.component.scss"], }) export class DataJobDetailsPageComponent - extends TaurusBaseComponent - implements OnInit, OnTaurusModelInit, OnTaurusModelLoad, OnTaurusModelChange, OnTaurusModelError + extends TaurusBaseComponent + implements + OnInit, + OnTaurusModelInit, + OnTaurusModelLoad, + OnTaurusModelChange, + OnTaurusModelError { - readonly uuid = 'DataJobDetailsPageComponent'; - - dataJobStatusEnum = DataJobStatus; - - jobName: string; - teamName: string; - jobState: DataJob; - jobDetails: DataJobDetails; - jobExecutions: DataJobExecutions = []; - - isJobEditable = false; - - /** - * ** Flag instruct whether template to show team section. - */ - shouldShowTeamsSection = false; - - /** - * ** Flag instruct whether template to show change history section. - */ - shouldShowChangeHistorySection = false; - - cronError: string = null; - - next: Date; - - loadingExecutions = true; - loadingInProgress = true; - allowExecutionsByDeployment = false; - - tmForm: FormGroup; - formState: VdkFormState; - readFormState: VdkFormState; - editableFormState: VdkFormState; - - canEditSection = true; - - collectorOptions: ModalOptions; - confirmationOptions: ModalOptions; - deleteOptions: ModalOptions; - executeNowOptions: ModalOptions; - - showFullDescription = false; - - descriptionWordsBeforeTruncate = 12; - - get name() { - return this.tmForm.get('name'); - } - - get team() { - return this.tmForm.get('team'); - } - - get status() { - return this.tmForm.get('status'); - } - - get description() { - return this.tmForm.get('description'); - } - - get jobPythonVersion() { - return this.tmForm.get('jobPythonVersion'); - } - - /** - * ** Array of error code patterns that component should listen for in errors store. - */ - listenForErrorPatterns: string[] = [LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE].All, LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS].All]; - - /** - * ** Flag that indicates there is jobs executions load error. - */ - isComponentInErrorState = false; - - /** - * ** Data Job Change history configuration. - */ - changeHistoryConfig: DataPipelinesConfig['changeHistory']; - - constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - private readonly router: Router, - private readonly routerService: RouterService, - private readonly dataJobsService: DataJobsService, - private readonly dataJobsApiService: DataJobsApiService, - private readonly formBuilder: FormBuilder, - private readonly toastService: ToastService, - private readonly errorHandlerService: ErrorHandlerService, - private readonly urlOpenerService: UrlOpenerService, - @Inject(DATA_PIPELINES_CONFIGS) - public readonly dataPipelinesModuleConfig: DataPipelinesConfig + readonly uuid = "DataJobDetailsPageComponent"; + + dataJobStatusEnum = DataJobStatus; + + jobName: string; + teamName: string; + jobState: DataJob; + jobDetails: DataJobDetails; + jobExecutions: DataJobExecutions = []; + + isJobEditable = false; + + /** + * ** Flag instruct whether template to show team section. + */ + shouldShowTeamsSection = false; + + /** + * ** Flag instruct whether template to show change history section. + */ + shouldShowChangeHistorySection = false; + + cronError: string = null; + + next: Date; + + loadingExecutions = true; + loadingInProgress = true; + allowExecutionsByDeployment = false; + + tmForm: FormGroup; + formState: VdkFormState; + readFormState: VdkFormState; + editableFormState: VdkFormState; + + canEditSection = true; + + collectorOptions: ModalOptions; + confirmationOptions: ModalOptions; + deleteOptions: ModalOptions; + executeNowOptions: ModalOptions; + + showFullDescription = false; + + descriptionWordsBeforeTruncate = 12; + + get name() { + return this.tmForm.get("name"); + } + + get team() { + return this.tmForm.get("team"); + } + + get status() { + return this.tmForm.get("status"); + } + + get description() { + return this.tmForm.get("description"); + } + + get jobPythonVersion() { + return this.tmForm.get("jobPythonVersion"); + } + + /** + * ** Array of error code patterns that component should listen for in errors store. + */ + listenForErrorPatterns: string[] = [ + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE].All, + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS].All, + ]; + + /** + * ** Flag that indicates there is jobs executions load error. + */ + isComponentInErrorState = false; + + /** + * ** Data Job Change history configuration. + */ + changeHistoryConfig: DataPipelinesConfig["changeHistory"]; + + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + private readonly router: Router, + private readonly routerService: RouterService, + private readonly dataJobsService: DataJobsService, + private readonly dataJobsApiService: DataJobsApiService, + private readonly formBuilder: FormBuilder, + private readonly toastService: ToastService, + private readonly errorHandlerService: ErrorHandlerService, + private readonly urlOpenerService: UrlOpenerService, + @Inject(DATA_PIPELINES_CONFIGS) + public readonly dataPipelinesModuleConfig: DataPipelinesConfig, + ) { + super(componentService, navigationService, activatedRoute); + + this.formState = new VdkFormState(FORM_STATE.VIEW); + this.readFormState = new VdkFormState(FORM_STATE.VIEW); + this.editableFormState = new VdkFormState(FORM_STATE.CAN_EDIT); + + this.confirmationOptions = new ConfirmationModalOptions(); + this.deleteOptions = new DeleteModalOptions(); + this.executeNowOptions = new ConfirmationModalOptions(); + + this._initForm(); + } + + isDescriptionSubmitEnabled(): boolean { + return ( + this._isFormSubmitEnabled() && + this.description.value !== this.jobDetails.description + ); + } + + isStatusSubmitEnabled(): boolean { + return ( + this._isFormSubmitEnabled() && + this.status.value !== + ExtractJobStatusPipe.transform(this.jobState?.deployments) + ); + } + + isJobRunning(): boolean { + return DataJobUtil.isJobRunning(this.jobExecutions); + } + + showNoNotificationsLabel(notifications: string[]): boolean { + return !notifications || notifications.length < 1; + } + + sectionStateChange(sectionState: VdkFormState) { + if (sectionState.state === FORM_STATE.CAN_EDIT) { + switch (sectionState.emittingSection) { + case "status": + this.status.setValue( + ExtractJobStatusPipe.transform(this.jobState?.deployments), + ); + break; + case "description": + this.description.setValue(this.jobDetails.description); + break; + default: + break; + } + this.canEditSection = true; + } else if (sectionState.state === FORM_STATE.SUBMIT) { + this.submitForm(sectionState); + } else if (sectionState.state === FORM_STATE.EDIT) { + this.canEditSection = false; + } + } + + submitForm(event: VdkFormState) { + if ( + event.emittingSection === "description" && + this.isDescriptionSubmitEnabled() ) { - super(componentService, navigationService, activatedRoute); - - this.formState = new VdkFormState(FORM_STATE.VIEW); - this.readFormState = new VdkFormState(FORM_STATE.VIEW); - this.editableFormState = new VdkFormState(FORM_STATE.CAN_EDIT); - - this.confirmationOptions = new ConfirmationModalOptions(); - this.deleteOptions = new DeleteModalOptions(); - this.executeNowOptions = new ConfirmationModalOptions(); - - this._initForm(); - } - - isDescriptionSubmitEnabled(): boolean { - return this._isFormSubmitEnabled() && this.description.value !== this.jobDetails.description; - } - - isStatusSubmitEnabled(): boolean { - return this._isFormSubmitEnabled() && this.status.value !== ExtractJobStatusPipe.transform(this.jobState?.deployments); - } - - isJobRunning(): boolean { - return DataJobUtil.isJobRunning(this.jobExecutions); - } - - showNoNotificationsLabel(notifications: string[]): boolean { - return !notifications || notifications.length < 1; - } - - sectionStateChange(sectionState: VdkFormState) { - if (sectionState.state === FORM_STATE.CAN_EDIT) { - switch (sectionState.emittingSection) { - case 'status': - this.status.setValue(ExtractJobStatusPipe.transform(this.jobState?.deployments)); - break; - case 'description': - this.description.setValue(this.jobDetails.description); - break; - default: - break; - } - this.canEditSection = true; - } else if (sectionState.state === FORM_STATE.SUBMIT) { - this.submitForm(sectionState); - } else if (sectionState.state === FORM_STATE.EDIT) { - this.canEditSection = false; - } - } - - submitForm(event: VdkFormState) { - if (event.emittingSection === 'description' && this.isDescriptionSubmitEnabled()) { - this._doSubmitDescriptionUpdate(); - } - - if (event.emittingSection === 'status' && this.isStatusSubmitEnabled()) { - this._doSubmitStatusUpdate(); - } - } - - editOperationEnded() { - this.formState = new VdkFormState(FORM_STATE.CAN_EDIT); - this.canEditSection = true; - } - - loadJobExecutions() { - this.dataJobsService.loadJobExecutions(this.model); - } - - redirectToHealthStatus() { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigateByUrl(StringUtil.stringFormat(this.dataPipelinesModuleConfig.healthStatusUrl, this.jobDetails.job_name)).then(); - } - - /** - * ** Intercepts click on change history link and show confirmation. - */ - navigateToJobChangeHistory($event: MouseEvent): void { - $event.preventDefault(); - $event.stopImmediatePropagation(); - - this.urlOpenerService - .open(this.changeHistoryConfig.urlTemplate, '_blank', { - title: this.changeHistoryConfig.confirmationTitle, - messageComponent: this.changeHistoryConfig.confirmationMessageComponent, - closable: true, - optionDoNotShowFutureConfirmation: true, - confirmBtnModel: { - text: 'Proceed', - iconShape: 'pop-out', - iconPosition: 'right' - } - }) - .then((_value) => { - // No-op. - }) - .catch((_reason) => { - // No-op. - }); - } - - /** - * @inheritDoc - */ - onModelInit(): void { - this.routerService - .getState() - .pipe(take(1)) - .subscribe((routeState) => this._initialize(routeState)); - } - - /** - * @inheritDoc - */ - onModelLoad(model: ComponentModel, task: string): void { - if (task === TASK_LOAD_JOB_EXECUTIONS) { - this.loadingExecutions = false; - } else if (task === TASK_LOAD_JOB_DETAILS) { - this.loadingInProgress = false; - } - } - - /** - * @inheritDoc - */ - onModelChange(model: ComponentModel, task: string): void { - if (task === TASK_LOAD_JOB_STATE) { - this.jobState = model.getComponentState().data.get(JOB_STATE_DATA_KEY); - this._initializeNextRunDate(); - this.allowExecutionsByDeployment = ExtractJobStatusPipe.transform(this.jobState?.deployments) !== DataJobStatus.NOT_DEPLOYED; - this.cronError = CronUtil.getNextExecutionErrors(this.jobState?.config?.schedule?.scheduleCron); - - return; - } - - if (task === TASK_LOAD_JOB_DETAILS) { - this.jobDetails = model.getComponentState().data.get(JOB_DETAILS_DATA_KEY); - this._updateForm(); - - return; - } - - if (task === TASK_LOAD_JOB_EXECUTIONS) { - const executions: DataJobExecutions = model.getComponentState().data.get(JOB_EXECUTIONS_DATA_KEY); - - if (executions) { - this.dataJobsService.notifyForJobExecutions([...executions]); - - // eslint-disable-next-line @typescript-eslint/unbound-method - const runningExecution = executions.find(DataJobUtil.isJobRunningPredicate); - if (runningExecution) { - this.dataJobsService.notifyForRunningJobExecutionId(runningExecution.id); - } - } - - return; - } - - if (task === TASK_UPDATE_JOB_DESCRIPTION) { - this.toastService.show({ - type: VmwToastType.INFO, - title: `Description update completed`, - description: `Data job "${this.jobName}" description successfully updated` - }); - - this.jobDetails = model.getComponentState().data.get(JOB_DETAILS_DATA_KEY); - - this.editOperationEnded(); - - return; - } - - if (task === TASK_UPDATE_JOB_STATUS) { - this.toastService.show({ - type: VmwToastType.INFO, - title: `Status update completed`, - description: - `Data job "${this.jobName}" successfully ` + `${!this._extractJobDeployment()?.enabled ? 'enabled' : 'disabled'}` - }); - - this.jobState = model.getComponentState().data.get(JOB_STATE_DATA_KEY); - - this.editOperationEnded(); - } - } - - /** - * @inheritDoc - */ - onModelError(model: ComponentModel, task: string, newErrorRecords: ErrorRecord[]): void { - newErrorRecords.forEach((errorRecord) => { - const error = ErrorUtil.extractError(errorRecord.error); - - let errorHandlerConfig: ErrorHandlerConfig; - - switch (task) { - case TASK_LOAD_JOB_DETAILS: - this._resetJobDetails(); - break; - case TASK_LOAD_JOB_EXECUTIONS: - // No-op. - break; - case TASK_LOAD_JOB_STATE: - // No-op. - break; - case TASK_UPDATE_JOB_DESCRIPTION: - errorHandlerConfig = { - title: 'Description update failed' - }; - this.editOperationEnded(); - break; - case TASK_UPDATE_JOB_STATUS: - errorHandlerConfig = { - title: 'Status update failed' - }; - this.editOperationEnded(); - break; - default: - // No-op. - } - - this.errorHandlerService.processError(error, errorHandlerConfig); - }); - } - - /** - * @inheritDoc - */ - override ngOnInit() { - // attach listener to ErrorStore and listen for Errors change - this.errors.onChange((store) => { - // if there is record for listened error code patterns set component in error state - this.isComponentInErrorState = store.hasCodePattern(...this.listenForErrorPatterns); - }); - - super.ngOnInit(); - - this._initializeNextRunDate(); - } - - private _initialize(state: RouteState): void { - const teamParamKey = state.getData('teamParamKey'); - this.teamName = state.getParam(teamParamKey); - - if (CollectionsUtil.isNil(teamParamKey) || CollectionsUtil.isNil(this.teamName)) { - this._subscribeForImplicitTeam(); - } - - const jobParamKey = state.getData('jobParamKey'); - this.jobName = state.getParam(jobParamKey); - - this.isJobEditable = !!state.getData('editable'); - - if (this.isJobEditable) { - this.formState = this.editableFormState; - } - - if (this.dataPipelinesModuleConfig) { - this._initializePageFeatureFlags(state); - this._initializeChangeHistoryConfig(); - } - - this._subscribeForExecutions(); - - this.dataJobsService.loadJob( - this.model - .withRequestParam(TEAM_NAME_REQ_PARAM, this.teamName) - .withRequestParam(JOB_NAME_REQ_PARAM, this.jobName) - .withRequestParam(ORDER_REQ_PARAM, { - property: 'startTime', - direction: ASC - } as DataJobExecutionOrder) - ); - } - - private _subscribeForImplicitTeam(): void { - this.dataJobsService - .getNotifiedForTeamImplicitly() - .pipe(take(1)) - .subscribe((teamName) => (this.teamName = teamName)); - } - - private _extractJobDeployment(): DataJobDeployment { - if (!this.jobState?.deployments) { - return null; - } - return this.jobState?.deployments[this.jobState?.deployments.length - 1]; - } - - private _isFormSubmitEnabled(): boolean { - return !this.tmForm?.pristine && this.tmForm?.valid; - } - - private _initForm(): void { - this.tmForm = this.formBuilder.group({ - name: '', - team: '', - status: '', - description: '', - jobPythonVersion: '' - }); - } - - private _updateForm(): void { - this.tmForm.setValue({ - name: this.jobDetails.job_name, - team: this.jobDetails.team, - status: ExtractJobStatusPipe.transform(this.jobState?.deployments), - description: this.jobDetails.description, - jobPythonVersion: - this.jobState?.deployments && this.jobState?.deployments[0] ? this.jobState?.deployments[0]?.jobPythonVersion : '' - }); - } - - private _doSubmitDescriptionUpdate(): void { - const jobDetailsUpdated: DataJobDetails = { - ...this.jobDetails, - description: this.description.value as string - }; - - this.dataJobsService.updateJob( - this.model - .withRequestParam(TEAM_NAME_REQ_PARAM, jobDetailsUpdated.team) - .withRequestParam(JOB_NAME_REQ_PARAM, jobDetailsUpdated.job_name) - .withRequestParam(JOB_DETAILS_REQ_PARAM, jobDetailsUpdated), - TASK_UPDATE_JOB_DESCRIPTION + this._doSubmitDescriptionUpdate(); + } + + if (event.emittingSection === "status" && this.isStatusSubmitEnabled()) { + this._doSubmitStatusUpdate(); + } + } + + editOperationEnded() { + this.formState = new VdkFormState(FORM_STATE.CAN_EDIT); + this.canEditSection = true; + } + + loadJobExecutions() { + this.dataJobsService.loadJobExecutions(this.model); + } + + redirectToHealthStatus() { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router + .navigateByUrl( + StringUtil.stringFormat( + this.dataPipelinesModuleConfig.healthStatusUrl, + this.jobDetails.job_name, + ), + ) + .then(); + } + + /** + * ** Intercepts click on change history link and show confirmation. + */ + navigateToJobChangeHistory($event: MouseEvent): void { + $event.preventDefault(); + $event.stopImmediatePropagation(); + + this.urlOpenerService + .open(this.changeHistoryConfig.urlTemplate, "_blank", { + title: this.changeHistoryConfig.confirmationTitle, + messageComponent: this.changeHistoryConfig.confirmationMessageComponent, + closable: true, + optionDoNotShowFutureConfirmation: true, + confirmBtnModel: { + text: "Proceed", + iconShape: "pop-out", + iconPosition: "right", + }, + }) + .then((_value) => { + // No-op. + }) + .catch((_reason) => { + // No-op. + }); + } + + /** + * @inheritDoc + */ + onModelInit(): void { + this.routerService + .getState() + .pipe(take(1)) + .subscribe((routeState) => this._initialize(routeState)); + } + + /** + * @inheritDoc + */ + onModelLoad(model: ComponentModel, task: string): void { + if (task === TASK_LOAD_JOB_EXECUTIONS) { + this.loadingExecutions = false; + } else if (task === TASK_LOAD_JOB_DETAILS) { + this.loadingInProgress = false; + } + } + + /** + * @inheritDoc + */ + onModelChange(model: ComponentModel, task: string): void { + if (task === TASK_LOAD_JOB_STATE) { + this.jobState = model.getComponentState().data.get(JOB_STATE_DATA_KEY); + this._initializeNextRunDate(); + this.allowExecutionsByDeployment = + ExtractJobStatusPipe.transform(this.jobState?.deployments) !== + DataJobStatus.NOT_DEPLOYED; + this.cronError = CronUtil.getNextExecutionErrors( + this.jobState?.config?.schedule?.scheduleCron, + ); + + return; + } + + if (task === TASK_LOAD_JOB_DETAILS) { + this.jobDetails = model + .getComponentState() + .data.get(JOB_DETAILS_DATA_KEY); + this._updateForm(); + + return; + } + + if (task === TASK_LOAD_JOB_EXECUTIONS) { + const executions: DataJobExecutions = model + .getComponentState() + .data.get(JOB_EXECUTIONS_DATA_KEY); + + if (executions) { + this.dataJobsService.notifyForJobExecutions([...executions]); + + // eslint-disable-next-line @typescript-eslint/unbound-method + const runningExecution = executions.find( + DataJobUtil.isJobRunningPredicate, ); - } - - private _doSubmitStatusUpdate(): void { - const jobDeployment = this._extractJobDeployment(); - - if (!jobDeployment) { - console.log('Status update will not be performed for job with no deployments.'); - - return; + if (runningExecution) { + this.dataJobsService.notifyForRunningJobExecutionId( + runningExecution.id, + ); } - - const jobState: DataJob = { - ...this.jobState, - deployments: [ - { - ...this.jobState.deployments[0], - enabled: this.status.value === DataJobStatus.ENABLED - }, - ...this.jobState.deployments.slice(1) - ] - }; - - this.dataJobsService.updateJob( - this.model - .withRequestParam(TEAM_NAME_REQ_PARAM, this.jobDetails.team) - .withRequestParam(JOB_NAME_REQ_PARAM, this.jobDetails.job_name) - .withRequestParam(JOB_DEPLOYMENT_ID_REQ_PARAM, jobDeployment.id) - .withRequestParam(JOB_STATUS_REQ_PARAM, this.status.value === DataJobStatus.ENABLED) - .withRequestParam(JOB_STATE_REQ_PARAM, jobState), - TASK_UPDATE_JOB_STATUS - ); - } - - private _initializeNextRunDate(): void { - this.next = ParseEpochPipe.transform(this.jobState?.config?.schedule?.nextRunEpochSeconds); - } - - private _initializePageFeatureFlags(state: RouteState): void { - if (state.getData<'explore' | 'manage'>('context') === 'explore') { - if (this.dataPipelinesModuleConfig.exploreConfig) { - this.shouldShowTeamsSection = this.dataPipelinesModuleConfig.exploreConfig.showTeamSectionInJobDetails; - this.shouldShowChangeHistorySection = this.dataPipelinesModuleConfig.exploreConfig.showChangeHistorySectionInJobDetails; - } - } else { - if (this.dataPipelinesModuleConfig.manageConfig) { - this.shouldShowTeamsSection = this.dataPipelinesModuleConfig.manageConfig.showTeamSectionInJobDetails; - this.shouldShowChangeHistorySection = this.dataPipelinesModuleConfig.manageConfig.showChangeHistorySectionInJobDetails; - } - } - } - - private _initializeChangeHistoryConfig(): void { - if ( - !this.shouldShowChangeHistorySection || - !this.dataPipelinesModuleConfig.changeHistory || - !this.dataPipelinesModuleConfig.changeHistory.urlTemplate || - !this.dataPipelinesModuleConfig.changeHistory.confirmationTitle - ) { - return; - } - - this.changeHistoryConfig = { - ...this.dataPipelinesModuleConfig.changeHistory, - urlTemplate: CollectionsUtil.interpolateString(this.dataPipelinesModuleConfig.changeHistory.urlTemplate, { - searchValue: '%data_job_name%', - replaceValue: this.jobName - }) - }; - } - - private _subscribeForExecutions(): void { - this.subscriptions.push( - this.dataJobsService.getNotifiedForJobExecutions().subscribe((executions) => { - this.jobExecutions = executions; - }) - ); - } - - private _resetJobDetails(): void { - if (!this.jobDetails) { - this.jobDetails = {}; - } - } + } + + return; + } + + if (task === TASK_UPDATE_JOB_DESCRIPTION) { + this.toastService.show({ + type: VmwToastType.INFO, + title: `Description update completed`, + description: `Data job "${this.jobName}" description successfully updated`, + }); + + this.jobDetails = model + .getComponentState() + .data.get(JOB_DETAILS_DATA_KEY); + + this.editOperationEnded(); + + return; + } + + if (task === TASK_UPDATE_JOB_STATUS) { + this.toastService.show({ + type: VmwToastType.INFO, + title: `Status update completed`, + description: + `Data job "${this.jobName}" successfully ` + + `${!this._extractJobDeployment()?.enabled ? "enabled" : "disabled"}`, + }); + + this.jobState = model.getComponentState().data.get(JOB_STATE_DATA_KEY); + + this.editOperationEnded(); + } + } + + /** + * @inheritDoc + */ + onModelError( + model: ComponentModel, + task: string, + newErrorRecords: ErrorRecord[], + ): void { + newErrorRecords.forEach((errorRecord) => { + const error = ErrorUtil.extractError(errorRecord.error); + + let errorHandlerConfig: ErrorHandlerConfig; + + switch (task) { + case TASK_LOAD_JOB_DETAILS: + this._resetJobDetails(); + break; + case TASK_LOAD_JOB_EXECUTIONS: + // No-op. + break; + case TASK_LOAD_JOB_STATE: + // No-op. + break; + case TASK_UPDATE_JOB_DESCRIPTION: + errorHandlerConfig = { + title: "Description update failed", + }; + this.editOperationEnded(); + break; + case TASK_UPDATE_JOB_STATUS: + errorHandlerConfig = { + title: "Status update failed", + }; + this.editOperationEnded(); + break; + default: + // No-op. + } + + this.errorHandlerService.processError(error, errorHandlerConfig); + }); + } + + /** + * @inheritDoc + */ + override ngOnInit() { + // attach listener to ErrorStore and listen for Errors change + this.errors.onChange((store) => { + // if there is record for listened error code patterns set component in error state + this.isComponentInErrorState = store.hasCodePattern( + ...this.listenForErrorPatterns, + ); + }); + + super.ngOnInit(); + + this._initializeNextRunDate(); + } + + private _initialize(state: RouteState): void { + const teamParamKey = state.getData("teamParamKey"); + this.teamName = state.getParam(teamParamKey); + + if ( + CollectionsUtil.isNil(teamParamKey) || + CollectionsUtil.isNil(this.teamName) + ) { + this._subscribeForImplicitTeam(); + } + + const jobParamKey = state.getData("jobParamKey"); + this.jobName = state.getParam(jobParamKey); + + this.isJobEditable = !!state.getData("editable"); + + if (this.isJobEditable) { + this.formState = this.editableFormState; + } + + if (this.dataPipelinesModuleConfig) { + this._initializePageFeatureFlags(state); + this._initializeChangeHistoryConfig(); + } + + this._subscribeForExecutions(); + + this.dataJobsService.loadJob( + this.model + .withRequestParam(TEAM_NAME_REQ_PARAM, this.teamName) + .withRequestParam(JOB_NAME_REQ_PARAM, this.jobName) + .withRequestParam(ORDER_REQ_PARAM, { + property: "startTime", + direction: ASC, + } as DataJobExecutionOrder), + ); + } + + private _subscribeForImplicitTeam(): void { + this.dataJobsService + .getNotifiedForTeamImplicitly() + .pipe(take(1)) + .subscribe((teamName) => (this.teamName = teamName)); + } + + private _extractJobDeployment(): DataJobDeployment { + if (!this.jobState?.deployments) { + return null; + } + return this.jobState?.deployments[this.jobState?.deployments.length - 1]; + } + + private _isFormSubmitEnabled(): boolean { + return !this.tmForm?.pristine && this.tmForm?.valid; + } + + private _initForm(): void { + this.tmForm = this.formBuilder.group({ + name: "", + team: "", + status: "", + description: "", + jobPythonVersion: "", + }); + } + + private _updateForm(): void { + this.tmForm.setValue({ + name: this.jobDetails.job_name, + team: this.jobDetails.team, + status: ExtractJobStatusPipe.transform(this.jobState?.deployments), + description: this.jobDetails.description, + jobPythonVersion: + this.jobState?.deployments && this.jobState?.deployments[0] + ? this.jobState?.deployments[0]?.jobPythonVersion + : "", + }); + } + + private _doSubmitDescriptionUpdate(): void { + const jobDetailsUpdated: DataJobDetails = { + ...this.jobDetails, + description: this.description.value as string, + }; + + this.dataJobsService.updateJob( + this.model + .withRequestParam(TEAM_NAME_REQ_PARAM, jobDetailsUpdated.team) + .withRequestParam(JOB_NAME_REQ_PARAM, jobDetailsUpdated.job_name) + .withRequestParam(JOB_DETAILS_REQ_PARAM, jobDetailsUpdated), + TASK_UPDATE_JOB_DESCRIPTION, + ); + } + + private _doSubmitStatusUpdate(): void { + const jobDeployment = this._extractJobDeployment(); + + if (!jobDeployment) { + console.log( + "Status update will not be performed for job with no deployments.", + ); + + return; + } + + const jobState: DataJob = { + ...this.jobState, + deployments: [ + { + ...this.jobState.deployments[0], + enabled: this.status.value === DataJobStatus.ENABLED, + }, + ...this.jobState.deployments.slice(1), + ], + }; + + this.dataJobsService.updateJob( + this.model + .withRequestParam(TEAM_NAME_REQ_PARAM, this.jobDetails.team) + .withRequestParam(JOB_NAME_REQ_PARAM, this.jobDetails.job_name) + .withRequestParam(JOB_DEPLOYMENT_ID_REQ_PARAM, jobDeployment.id) + .withRequestParam( + JOB_STATUS_REQ_PARAM, + this.status.value === DataJobStatus.ENABLED, + ) + .withRequestParam(JOB_STATE_REQ_PARAM, jobState), + TASK_UPDATE_JOB_STATUS, + ); + } + + private _initializeNextRunDate(): void { + this.next = ParseEpochPipe.transform( + this.jobState?.config?.schedule?.nextRunEpochSeconds, + ); + } + + private _initializePageFeatureFlags(state: RouteState): void { + if (state.getData<"explore" | "manage">("context") === "explore") { + if (this.dataPipelinesModuleConfig.exploreConfig) { + this.shouldShowTeamsSection = + this.dataPipelinesModuleConfig.exploreConfig.showTeamSectionInJobDetails; + this.shouldShowChangeHistorySection = + this.dataPipelinesModuleConfig.exploreConfig.showChangeHistorySectionInJobDetails; + } + } else { + if (this.dataPipelinesModuleConfig.manageConfig) { + this.shouldShowTeamsSection = + this.dataPipelinesModuleConfig.manageConfig.showTeamSectionInJobDetails; + this.shouldShowChangeHistorySection = + this.dataPipelinesModuleConfig.manageConfig.showChangeHistorySectionInJobDetails; + } + } + } + + private _initializeChangeHistoryConfig(): void { + if ( + !this.shouldShowChangeHistorySection || + !this.dataPipelinesModuleConfig.changeHistory || + !this.dataPipelinesModuleConfig.changeHistory.urlTemplate || + !this.dataPipelinesModuleConfig.changeHistory.confirmationTitle + ) { + return; + } + + this.changeHistoryConfig = { + ...this.dataPipelinesModuleConfig.changeHistory, + urlTemplate: CollectionsUtil.interpolateString( + this.dataPipelinesModuleConfig.changeHistory.urlTemplate, + { + searchValue: "%data_job_name%", + replaceValue: this.jobName, + }, + ), + }; + } + + private _subscribeForExecutions(): void { + this.subscriptions.push( + this.dataJobsService + .getNotifiedForJobExecutions() + .subscribe((executions) => { + this.jobExecutions = executions; + }), + ); + } + + private _resetJobDetails(): void { + if (!this.jobDetails) { + this.jobDetails = {}; + } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/index.ts index 53e6f21f80..31c4d5a788 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-details-page.component'; +export * from "./data-job-details-page.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/details/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-deployment-details-modal/data-job-deployment-details-modal.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-deployment-details-modal/data-job-deployment-details-modal.component.html index c2e81dcd3f..b9d9f6e37d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-deployment-details-modal/data-job-deployment-details-modal.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-deployment-details-modal/data-job-deployment-details-modal.component.html @@ -4,88 +4,93 @@ --> - - diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.scss index 7f0b1219ba..c94653f59c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.scss @@ -4,29 +4,29 @@ */ .job-execution-status__container { - display: flex; - align-items: baseline; - flex-direction: column; + display: flex; + align-items: baseline; + flex-direction: column; - .job-execution-status__text { - margin-left: 0.25rem; - } + .job-execution-status__text { + margin-left: 0.25rem; + } - .job-execution-status__btn { - padding: 0; - margin-left: 5px; - text-transform: lowercase; - font-size: 12px; - outline-color: transparent; - } + .job-execution-status__btn { + padding: 0; + margin-left: 5px; + text-transform: lowercase; + font-size: 12px; + outline-color: transparent; + } } :host { - display: block; - width: 100%; - height: 100%; + display: block; + width: 100%; + height: 100%; } .job-execution-signpost { - cursor: auto; + cursor: auto; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts index 84b75d25f9..2f1bc60108 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts @@ -3,93 +3,101 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input } from '@angular/core'; +import { Component, Input } from "@angular/core"; -import { DataJobExecutionStatus } from '../../../../../model'; +import { DataJobExecutionStatus } from "../../../../../model"; type StatusPropertiesMapping = { - shape: string; - status: string; - direction: string; - text: string; + shape: string; + status: string; + direction: string; + text: string; }; @Component({ - selector: 'lib-data-job-execution-status', - templateUrl: './data-job-execution-status.component.html', - styleUrls: ['./data-job-execution-status.component.scss'] + selector: "lib-data-job-execution-status", + templateUrl: "./data-job-execution-status.component.html", + styleUrls: ["./data-job-execution-status.component.scss"], }) export class DataJobExecutionStatusComponent { - @Input() jobStatus: DataJobExecutionStatus; - @Input() jobMessage = ''; - @Input() showErrorMessage = false; - clrOpen = false; + @Input() jobStatus: DataJobExecutionStatus; + @Input() jobMessage = ""; + @Input() showErrorMessage = false; + clrOpen = false; - statusPropertiesMapping: { [key: string]: StatusPropertiesMapping } = { - [DataJobExecutionStatus.SUBMITTED]: { - shape: 'hourglass', - status: '', - direction: '', - text: 'Submitted' - }, - [DataJobExecutionStatus.RUNNING]: { - shape: 'play', - status: '', - direction: '', - text: 'Running' - }, - [DataJobExecutionStatus.SUCCEEDED]: { - shape: 'success-standard', - status: 'is-success', - direction: '', - text: 'Success' - }, - [DataJobExecutionStatus.CANCELLED]: { - shape: 'ban', - status: '', - direction: '', - text: 'Canceled' - }, - [DataJobExecutionStatus.SKIPPED]: { - shape: 'angle-double', - status: '', - direction: 'right', - text: 'Skipped' - }, - [DataJobExecutionStatus.USER_ERROR]: { - shape: 'error-standard', - status: 'is-danger', - direction: '', - text: 'User Error' - }, - [DataJobExecutionStatus.PLATFORM_ERROR]: { - shape: 'error-standard', - status: 'is-warning', - direction: '', - text: 'Platform Error' - }, - [DataJobExecutionStatus.FAILED]: { - shape: 'error-standard', - status: 'is-danger', - direction: '', - text: 'Error' - } - }; + statusPropertiesMapping: { [key: string]: StatusPropertiesMapping } = { + [DataJobExecutionStatus.SUBMITTED]: { + shape: "hourglass", + status: "", + direction: "", + text: "Submitted", + }, + [DataJobExecutionStatus.RUNNING]: { + shape: "play", + status: "", + direction: "", + text: "Running", + }, + [DataJobExecutionStatus.SUCCEEDED]: { + shape: "success-standard", + status: "is-success", + direction: "", + text: "Success", + }, + [DataJobExecutionStatus.CANCELLED]: { + shape: "ban", + status: "", + direction: "", + text: "Canceled", + }, + [DataJobExecutionStatus.SKIPPED]: { + shape: "angle-double", + status: "", + direction: "right", + text: "Skipped", + }, + [DataJobExecutionStatus.USER_ERROR]: { + shape: "error-standard", + status: "is-danger", + direction: "", + text: "User Error", + }, + [DataJobExecutionStatus.PLATFORM_ERROR]: { + shape: "error-standard", + status: "is-warning", + direction: "", + text: "Platform Error", + }, + [DataJobExecutionStatus.FAILED]: { + shape: "error-standard", + status: "is-danger", + direction: "", + text: "Error", + }, + }; - get executionStatusProperties(): StatusPropertiesMapping { - return this.statusPropertiesMapping[this.jobStatus] ?? ({} as StatusPropertiesMapping); - } + get executionStatusProperties(): StatusPropertiesMapping { + return ( + this.statusPropertiesMapping[this.jobStatus] ?? + ({} as StatusPropertiesMapping) + ); + } - isJobStatusSuitableForMessageTooltip(): boolean { - return ( - this.jobStatus === DataJobExecutionStatus.PLATFORM_ERROR || - this.jobStatus === DataJobExecutionStatus.USER_ERROR || - this.jobStatus === DataJobExecutionStatus.SKIPPED - ); - } + isJobStatusSuitableForMessageTooltip(): boolean { + return ( + this.jobStatus === DataJobExecutionStatus.PLATFORM_ERROR || + this.jobStatus === DataJobExecutionStatus.USER_ERROR || + this.jobStatus === DataJobExecutionStatus.SKIPPED + ); + } - isJobMessageDifferentFromStatus(): boolean { - const message = this.jobMessage?.toLowerCase(); - return message !== 'user error' && message !== 'platform error' && message !== 'skipped' && message !== ''; - } + isJobMessageDifferentFromStatus(): boolean { + const message = this.jobMessage?.toLowerCase(); + return ( + message !== "user error" && + message !== "platform error" && + message !== "skipped" && + message !== "" + ); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/index.ts index 29df82188c..2d734a7caf 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-execution-status.component'; +export * from "./data-job-execution-status.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.html index 12b8297a82..273326d9d8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.html @@ -4,20 +4,24 @@ --> - + - + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.ts index 61e8321531..39b82c1f30 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/data-job-execution-type-filter.component.ts @@ -3,113 +3,122 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, +} from "@angular/core"; -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject } from "rxjs"; -import { ClrDatagridFilterInterface } from '@clr/angular'; +import { ClrDatagridFilterInterface } from "@clr/angular"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { DataJobExecutionType } from '../../../../../model'; +import { DataJobExecutionType } from "../../../../../model"; -import { GridDataJobExecution } from '../model/data-job-execution'; +import { GridDataJobExecution } from "../model/data-job-execution"; @Component({ - selector: 'lib-data-job-execution-type-filter', - templateUrl: './data-job-execution-type-filter.component.html' + selector: "lib-data-job-execution-type-filter", + templateUrl: "./data-job-execution-type-filter.component.html", }) -export class DataJobExecutionTypeFilterComponent implements OnChanges, ClrDatagridFilterInterface { - /** - * ** Path to value (property). - */ - @Input() property: string; - - /** - * ** Value bound to {@link property}. - */ - @Input() value: string; - - /** - * ** Event emitter that emits whenever {@link value} change. - */ - @Output() valueChange = new EventEmitter(); - - allTypes = [DataJobExecutionType.MANUAL, DataJobExecutionType.SCHEDULED]; - selectedTypes: string[] = []; - - // We do not want to expose the Subject itself, but the Observable which is read-only - get changes(): Observable { - return this._changesSubject.asObservable(); +export class DataJobExecutionTypeFilterComponent + implements OnChanges, ClrDatagridFilterInterface +{ + /** + * ** Path to value (property). + */ + @Input() property: string; + + /** + * ** Value bound to {@link property}. + */ + @Input() value: string; + + /** + * ** Event emitter that emits whenever {@link value} change. + */ + @Output() valueChange = new EventEmitter(); + + allTypes = [DataJobExecutionType.MANUAL, DataJobExecutionType.SCHEDULED]; + selectedTypes: string[] = []; + + // We do not want to expose the Subject itself, but the Observable which is read-only + get changes(): Observable { + return this._changesSubject.asObservable(); + } + + private _changesSubject = new Subject(); + + isActive(): boolean { + return this.selectedTypes.length > 0; + } + + accepts(item: GridDataJobExecution): boolean { + return this.selectedTypes.indexOf(item.type) > -1; + } + + toggleCheckbox(event: Event) { + const checkbox = event.target as HTMLInputElement; + + if (checkbox.checked) { + this.selectedTypes.push(checkbox.value); + } else { + const statusToRemoveIndex = this.selectedTypes.indexOf(checkbox.value); + if (statusToRemoveIndex > -1) { + this.selectedTypes.splice(statusToRemoveIndex, 1); + } } - private _changesSubject = new Subject(); + this._updateValue(true); + } - isActive(): boolean { - return this.selectedTypes.length > 0; + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + if (changes["value"]) { + this._refreshValue(); } + } - accepts(item: GridDataJobExecution): boolean { - return this.selectedTypes.indexOf(item.type) > -1; - } - - toggleCheckbox(event: Event) { - const checkbox = event.target as HTMLInputElement; + private _refreshValue(): void { + const selectedTypes: string[] = []; - if (checkbox.checked) { - this.selectedTypes.push(checkbox.value); - } else { - const statusToRemoveIndex = this.selectedTypes.indexOf(checkbox.value); - if (statusToRemoveIndex > -1) { - this.selectedTypes.splice(statusToRemoveIndex, 1); - } + if (CollectionsUtil.isStringWithContent(this.value)) { + const checkedValues = this._deserializeTypes(); + for (const checkedValue of checkedValues) { + if (this.allTypes.includes(checkedValue)) { + selectedTypes.push(checkedValue); } - - this._updateValue(true); + } } - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges): void { - if (changes['value']) { - this._refreshValue(); - } - } + this.selectedTypes = selectedTypes; - private _refreshValue(): void { - const selectedTypes: string[] = []; + this._updateValue(); + } - if (CollectionsUtil.isStringWithContent(this.value)) { - const checkedValues = this._deserializeTypes(); - for (const checkedValue of checkedValues) { - if (this.allTypes.includes(checkedValue)) { - selectedTypes.push(checkedValue); - } - } - } + private _updateValue(notifyChange = false): void { + const serializedValue = this._serializeTypes(); - this.selectedTypes = selectedTypes; + this.value = serializedValue; + this._changesSubject.next(serializedValue); - this._updateValue(); + if (notifyChange) { + this.valueChange.next(serializedValue); } + } - private _updateValue(notifyChange = false): void { - const serializedValue = this._serializeTypes(); - - this.value = serializedValue; - this._changesSubject.next(serializedValue); + private _serializeTypes(): string { + return this.selectedTypes.join(",").toLowerCase(); + } - if (notifyChange) { - this.valueChange.next(serializedValue); - } - } - - private _serializeTypes(): string { - return this.selectedTypes.join(',').toLowerCase(); - } - - private _deserializeTypes(): DataJobExecutionType[] { - return this.value.toUpperCase().split(',') as DataJobExecutionType[]; - } + private _deserializeTypes(): DataJobExecutionType[] { + return this.value.toUpperCase().split(",") as DataJobExecutionType[]; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/index.ts index 6d48e89a06..a2168b17aa 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type-filter/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-execution-type-filter.component'; +export * from "./data-job-execution-type-filter.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.html index c16e4efbd5..d05d6b3947 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.html @@ -4,15 +4,15 @@ --> - + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.ts index 8e75d68ee0..02158ed24f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/data-job-execution-type.component.ts @@ -3,28 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input } from '@angular/core'; +import { Component, Input } from "@angular/core"; -import { DataJobExecutionType } from '../../../../../model'; +import { DataJobExecutionType } from "../../../../../model"; -import { GridDataJobExecution } from '../model/data-job-execution'; +import { GridDataJobExecution } from "../model/data-job-execution"; @Component({ - selector: 'lib-data-job-execution-type', - templateUrl: './data-job-execution-type.component.html' + selector: "lib-data-job-execution-type", + templateUrl: "./data-job-execution-type.component.html", }) export class DataJobExecutionTypeComponent { - @Input() jobExecution: GridDataJobExecution; + @Input() jobExecution: GridDataJobExecution; - executionTypePropertiesMapping = { - [DataJobExecutionType.MANUAL]: { - shape: 'cursor-hand-open', - status: 'is-info' - }, - [DataJobExecutionType.SCHEDULED]: { shape: 'clock', status: '' } - }; + executionTypePropertiesMapping = { + [DataJobExecutionType.MANUAL]: { + shape: "cursor-hand-open", + status: "is-info", + }, + [DataJobExecutionType.SCHEDULED]: { shape: "clock", status: "" }, + }; - get executionTypeProperties() { - return this.executionTypePropertiesMapping[this.jobExecution.type]; - } + get executionTypeProperties() { + return this.executionTypePropertiesMapping[this.jobExecution.type]; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/index.ts index 107d5d1540..0c26e44d20 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-type/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-execution-type.component'; +export * from "./data-job-execution-type.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.spec.ts index 15dec71f89..06cf7ee5f2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.spec.ts @@ -3,99 +3,117 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionDateComparator } from './execution-date.comparator'; - -describe('ExecutionDateComparator', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const aStartTime = new Date(); - const bStartTime = new Date(aStartTime.getTime() + 100); - const aEndTime = new Date(); - const bEndTime = new Date(aEndTime.getTime() + 110); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: '' - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: '' - } - ]; - }); +import { DatePipe } from "@angular/common"; - describe('Properties::', () => { - describe('|property|', () => { - it('should verify value', () => { - // Given - const instance = new ExecutionDateComparator('endTime', 'ASC'); - - // Then - expect(instance.property).toEqual('endTime'); - }); - }); - - describe('|direction|', () => { - it('should verify value', () => { - // Given - const instance = new ExecutionDateComparator('startTime', 'ASC'); - - // Then - expect(instance.direction).toEqual('ASC'); - }); - }); - }); +import { TestBed } from "@angular/core/testing"; - describe('Methods::', () => { - describe('|compare|', () => { - it('should verify will return -100 because of ascending sort', () => { - // Given - const instance = new ExecutionDateComparator('startTime', 'ASC'); +import { DATA_PIPELINES_DATE_TIME_FORMAT } from "../../../../../../../model"; - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); +import { GridDataJobExecution } from "../../../model"; - // Then - expect(res).toEqual(-100); - }); +import { ExecutionDateComparator } from "./execution-date.comparator"; - it('should verify will return 110 because of descending sort', () => { - // Given - const instance = new ExecutionDateComparator('endTime', 'DESC'); +describe("ExecutionDateComparator", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual(110); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const bStartTime = new Date(aStartTime.getTime() + 100); + const aEndTime = new Date(); + const bEndTime = new Date(aEndTime.getTime() + 110); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "", + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "", + }, + ]; + }); + + describe("Properties::", () => { + describe("|property|", () => { + it("should verify value", () => { + // Given + const instance = new ExecutionDateComparator("endTime", "ASC"); + + // Then + expect(instance.property).toEqual("endTime"); + }); + }); + + describe("|direction|", () => { + it("should verify value", () => { + // Given + const instance = new ExecutionDateComparator("startTime", "ASC"); + + // Then + expect(instance.direction).toEqual("ASC"); + }); + }); + }); + + describe("Methods::", () => { + describe("|compare|", () => { + it("should verify will return -100 because of ascending sort", () => { + // Given + const instance = new ExecutionDateComparator("startTime", "ASC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(-100); + }); + + it("should verify will return 110 because of descending sort", () => { + // Given + const instance = new ExecutionDateComparator("endTime", "DESC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(110); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.ts index f7de694b87..6e7bc4ae3e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/execution-date.comparator.ts @@ -3,41 +3,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { get } from 'lodash'; +import { get } from "lodash"; -import { Comparator } from '@versatiledatakit/shared'; +import { Comparator } from "@versatiledatakit/shared"; -import { GridDataJobExecution } from '../../../model/data-job-execution'; +import { GridDataJobExecution } from "../../../model/data-job-execution"; export class ExecutionDateComparator implements Comparator { - /** - * ** Property path to value from GridDataJobExecution object. - */ - public readonly property: keyof GridDataJobExecution; - - /** - * ** Sort direction. - */ - public readonly direction: 'ASC' | 'DESC'; - - /** - * ** Constructor. - */ - constructor(property: keyof GridDataJobExecution, direction: 'ASC' | 'DESC') { - this.property = property; - this.direction = direction; - } - - /** - * @inheritDoc - */ - compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution): number { - const value1 = get(exec1, this.property) as string; - const value2 = get(exec2, this.property) as string; - - const date1 = value1 ? Date.parse(value1) : Date.now(); - const date2 = value2 ? Date.parse(value2) : Date.now(); - - return this.direction === 'ASC' ? date1 - date2 : date2 - date1; - } + /** + * ** Property path to value from GridDataJobExecution object. + */ + public readonly property: keyof GridDataJobExecution; + + /** + * ** Sort direction. + */ + public readonly direction: "ASC" | "DESC"; + + /** + * ** Constructor. + */ + constructor(property: keyof GridDataJobExecution, direction: "ASC" | "DESC") { + this.property = property; + this.direction = direction; + } + + /** + * @inheritDoc + */ + compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution): number { + const value1 = get( + exec1, + this.property, + ) as string; + const value2 = get( + exec2, + this.property, + ) as string; + + const date1 = value1 ? Date.parse(value1) : Date.now(); + const date2 = value2 ? Date.parse(value2) : Date.now(); + + return this.direction === "ASC" ? date1 - date2 : date2 - date1; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/index.ts index 97e38c6d3a..4d230fac89 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/date/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './execution-date.comparator'; +export * from "./execution-date.comparator"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts index a8b51425df..d31fc31532 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts @@ -3,134 +3,164 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionDefaultComparator } from './execution-default.comparator'; - -describe('ExecutionDefaultComparator', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const aStartTime = new Date(); - const aEndTime = new Date(aStartTime.getTime() + 100); - const bStartTime = new Date(); - const bEndTime = new Date(bStartTime.getTime() + 110); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: 'version-10' - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: 'version-11' - } - ]; - }); - - describe('Properties::', () => { - describe('|direction|', () => { - it('should verify value', () => { - // Given - const instance = new ExecutionDefaultComparator('jobVersion', 'ASC'); - - // Then - expect(instance.property).toEqual('jobVersion'); - expect(instance.direction).toEqual('ASC'); - }); - }); - }); - - describe('Methods::', () => { - describe('|compare|', () => { - it('should verify will return -1 because of ascending sort', () => { - // Given - const instance = new ExecutionDefaultComparator('jobVersion', 'ASC'); - - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); - - // Then - expect(res).toEqual(-1); - }); - - it('should verify will return 1 because of ascending sort', () => { - // Given - const instance = new ExecutionDefaultComparator('endTime', 'ASC'); - - // When - const res = instance.compare(dataJobExecutions[1], dataJobExecutions[0]); +import { DatePipe } from "@angular/common"; - // Then - expect(res).toEqual(1); - }); +import { TestBed } from "@angular/core/testing"; - it('should verify will return 1 because of descending sort', () => { - // Given - const instance = new ExecutionDefaultComparator('jobVersion', 'DESC'); +import { DATA_PIPELINES_DATE_TIME_FORMAT } from "../../../../../../../model"; - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); +import { GridDataJobExecution } from "../../../model"; - // Then - expect(res).toEqual(1); - }); +import { ExecutionDefaultComparator } from "./execution-default.comparator"; - it('should verify will return -1 because of descending sort', () => { - // Given - const instance = new ExecutionDefaultComparator('endTime', 'DESC'); +describe("ExecutionDefaultComparator", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // When - const res = instance.compare(dataJobExecutions[1], dataJobExecutions[0]); - - // Then - expect(res).toEqual(-1); - }); - - it('should verify will return 0 because of ascending sort', () => { - // Given - const instance = new ExecutionDefaultComparator('startTime', 'ASC'); - - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); - - // Then - expect(res).toEqual(-0); - }); - - it('should verify will return 0 because of descending sort', () => { - // Given - const instance = new ExecutionDefaultComparator('startTime', 'DESC'); - - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual(0); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "version-10", + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "version-11", + }, + ]; + }); + + describe("Properties::", () => { + describe("|direction|", () => { + it("should verify value", () => { + // Given + const instance = new ExecutionDefaultComparator("jobVersion", "ASC"); + + // Then + expect(instance.property).toEqual("jobVersion"); + expect(instance.direction).toEqual("ASC"); + }); + }); + }); + + describe("Methods::", () => { + describe("|compare|", () => { + it("should verify will return -1 because of ascending sort", () => { + // Given + const instance = new ExecutionDefaultComparator("jobVersion", "ASC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(-1); + }); + + it("should verify will return 1 because of ascending sort", () => { + // Given + const instance = new ExecutionDefaultComparator("endTime", "ASC"); + + // When + const res = instance.compare( + dataJobExecutions[1], + dataJobExecutions[0], + ); + + // Then + expect(res).toEqual(1); + }); + + it("should verify will return 1 because of descending sort", () => { + // Given + const instance = new ExecutionDefaultComparator("jobVersion", "DESC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(1); + }); + + it("should verify will return -1 because of descending sort", () => { + // Given + const instance = new ExecutionDefaultComparator("endTime", "DESC"); + + // When + const res = instance.compare( + dataJobExecutions[1], + dataJobExecutions[0], + ); + + // Then + expect(res).toEqual(-1); + }); + + it("should verify will return 0 because of ascending sort", () => { + // Given + const instance = new ExecutionDefaultComparator("startTime", "ASC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(-0); + }); + + it("should verify will return 0 because of descending sort", () => { + // Given + const instance = new ExecutionDefaultComparator("startTime", "DESC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(0); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts index 15005aa9c0..cccc51b34a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts @@ -3,42 +3,48 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { get } from 'lodash'; +import { get } from "lodash"; -import { Comparator } from '@versatiledatakit/shared'; +import { Comparator } from "@versatiledatakit/shared"; -import { GridDataJobExecution } from '../../../model/data-job-execution'; +import { GridDataJobExecution } from "../../../model/data-job-execution"; /** * ** Execution default comparator. */ export class ExecutionDefaultComparator implements Comparator { - /** - * ** Property path to value from GridDataJobExecution object. - */ - public readonly property: keyof GridDataJobExecution; - - /** - * ** Sort direction. - */ - public readonly direction: 'ASC' | 'DESC'; - - /** - * ** Constructor. - */ - constructor(property: keyof GridDataJobExecution, direction: 'ASC' | 'DESC') { - this.property = property; - this.direction = direction; - } - - /** - * @inheritDoc - */ - compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution) { - const value1 = get(exec1, this.property); - const value2 = get(exec2, this.property); - const directionModifier = this.direction === 'DESC' ? 1 : -1; - - return (value1 > value2 ? -1 : value2 > value1 ? 1 : 0) * directionModifier; - } + /** + * ** Property path to value from GridDataJobExecution object. + */ + public readonly property: keyof GridDataJobExecution; + + /** + * ** Sort direction. + */ + public readonly direction: "ASC" | "DESC"; + + /** + * ** Constructor. + */ + constructor(property: keyof GridDataJobExecution, direction: "ASC" | "DESC") { + this.property = property; + this.direction = direction; + } + + /** + * @inheritDoc + */ + compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution) { + const value1 = get( + exec1, + this.property, + ); + const value2 = get( + exec2, + this.property, + ); + const directionModifier = this.direction === "DESC" ? 1 : -1; + + return (value1 > value2 ? -1 : value2 > value1 ? 1 : 0) * directionModifier; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts index e0ce93af42..4ae298064a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './execution-default.comparator'; +export * from "./execution-default.comparator"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts index c47e0e4d30..2a5535f4c0 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts @@ -3,89 +3,107 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionDurationComparator } from './execution-duration.comparator'; - -describe('ExecutionDurationComparator', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const aStartTime = new Date(); - const aEndTime = new Date(aStartTime.getTime() + 100); - const bStartTime = new Date(); - const bEndTime = new Date(bStartTime.getTime() + 110); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: '' - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: '' - } - ]; - }); - - describe('Properties::', () => { - describe('|direction|', () => { - it('should verify value', () => { - // Given - const instance = new ExecutionDurationComparator('ASC'); +import { DatePipe } from "@angular/common"; - // Then - expect(instance.direction).toEqual('ASC'); - }); - }); - }); +import { TestBed } from "@angular/core/testing"; - describe('Methods::', () => { - describe('|compare|', () => { - it('should verify will return -10 because of ascending sort', () => { - // Given - const instance = new ExecutionDurationComparator('ASC'); +import { DATA_PIPELINES_DATE_TIME_FORMAT } from "../../../../../../../model"; - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); +import { GridDataJobExecution } from "../../../model"; - // Then - expect(res).toEqual(-10); - }); +import { ExecutionDurationComparator } from "./execution-duration.comparator"; - it('should verify will return 10 because of descending sort', () => { - // Given - const instance = new ExecutionDurationComparator('DESC'); +describe("ExecutionDurationComparator", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // When - const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual(10); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "", + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "", + }, + ]; + }); + + describe("Properties::", () => { + describe("|direction|", () => { + it("should verify value", () => { + // Given + const instance = new ExecutionDurationComparator("ASC"); + + // Then + expect(instance.direction).toEqual("ASC"); + }); + }); + }); + + describe("Methods::", () => { + describe("|compare|", () => { + it("should verify will return -10 because of ascending sort", () => { + // Given + const instance = new ExecutionDurationComparator("ASC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(-10); + }); + + it("should verify will return 10 because of descending sort", () => { + // Given + const instance = new ExecutionDurationComparator("DESC"); + + // When + const res = instance.compare( + dataJobExecutions[0], + dataJobExecutions[1], + ); + + // Then + expect(res).toEqual(10); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts index 39b925972f..15451c77ba 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts @@ -3,27 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparator } from '@versatiledatakit/shared'; +import { Comparator } from "@versatiledatakit/shared"; -import { GridDataJobExecution } from '../../../model/data-job-execution'; +import { GridDataJobExecution } from "../../../model/data-job-execution"; export class ExecutionDurationComparator implements Comparator { - public readonly direction: 'ASC' | 'DESC'; + public readonly direction: "ASC" | "DESC"; - /** - * ** Constructor. - */ - constructor(direction: 'ASC' | 'DESC') { - this.direction = direction; - } + /** + * ** Constructor. + */ + constructor(direction: "ASC" | "DESC") { + this.direction = direction; + } - /** - * @inheritDoc - */ - compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution): number { - const aDuration = Date.parse(exec1.endTime) - Date.parse(exec1.startTime); - const bDuration = Date.parse(exec2.endTime) - Date.parse(exec2.startTime); + /** + * @inheritDoc + */ + compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution): number { + const aDuration = Date.parse(exec1.endTime) - Date.parse(exec1.startTime); + const bDuration = Date.parse(exec2.endTime) - Date.parse(exec2.startTime); - return this.direction === 'ASC' ? aDuration - bDuration : bDuration - aDuration; - } + return this.direction === "ASC" + ? aDuration - bDuration + : bDuration - aDuration; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts index 88ed18067a..2102a0a40f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './execution-duration.comparator'; +export * from "./execution-duration.comparator"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts index 6cfbba3632..8ba9f08393 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './default'; -export * from './date'; -export * from './duration'; +export * from "./default"; +export * from "./date"; +export * from "./duration"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts index 03cf6e95b7..45d42356d6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './status'; -export * from './string'; -export * from './type'; +export * from "./status"; +export * from "./string"; +export * from "./type"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts index 79e132c1a3..0a2ee386a9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts @@ -3,153 +3,176 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { CallFake, CollectionsUtil } from '@versatiledatakit/shared'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionsStatusCriteria } from './executions-status.criteria'; - -describe('ExecutionsStatusCriteria', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const aStartTime = new Date(); - const aEndTime = new Date(aStartTime.getTime() + 100); - const bStartTime = new Date(); - const bEndTime = new Date(bStartTime.getTime() + 110); - const cStartTime = new Date(); - const cEndTime = new Date(cStartTime.getTime() + 120); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: '', - status: DataJobExecutionStatus.SUCCEEDED - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: '', - status: DataJobExecutionStatus.RUNNING - }, - { - id: 'cJob', - startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: cStartTime.toISOString(), - endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: cEndTime.toISOString(), - duration: '120', - jobVersion: '', - status: DataJobExecutionStatus.PLATFORM_ERROR - } - ]; - }); - - describe('Methods::', () => { - describe('|meetCriteria|', () => { - it('should verify will return Array with aJob and bJob', () => { - // Given - const instance = new ExecutionsStatusCriteria('succeeded,running'); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); - }); - - it('should verify will return Array with cJob', () => { - // Given - const instance = new ExecutionsStatusCriteria('platform_error'); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual([dataJobExecutions[2]]); - }); - - it('should verify will return empty Array', () => { - // Given - const instance = new ExecutionsStatusCriteria('user_error'); +import { DatePipe } from "@angular/common"; - // When - const res = instance.meetCriteria(dataJobExecutions); +import { TestBed } from "@angular/core/testing"; - // Then - expect(res).toEqual([]); - }); +import { CallFake, CollectionsUtil } from "@versatiledatakit/shared"; - it('should verify will return Array with all Jobs when serialized status criteria is empty string', () => { - // Given - const instance = new ExecutionsStatusCriteria(''); +import { + DATA_PIPELINES_DATE_TIME_FORMAT, + DataJobExecutionStatus, +} from "../../../../../../../model"; - // When - const res = instance.meetCriteria(dataJobExecutions); +import { GridDataJobExecution } from "../../../model"; - // Then - expect(res).toEqual(dataJobExecutions); - }); +import { ExecutionsStatusCriteria } from "./executions-status.criteria"; - it('should verify will return Array with all Jobs when serialized status criteria is Nil', () => { - // Given - const instance = new ExecutionsStatusCriteria(null); +describe("ExecutionsStatusCriteria", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual(dataJobExecutions); - }); - - it('should verify will return empty Array when Executions are Nil', () => { - // Given - const instance = new ExecutionsStatusCriteria('running'); - - // When - const res = instance.meetCriteria(null); - - // Then - expect(res).toEqual([]); - }); - - it('should verify will return Array with all Jobs when serialized status deserialization fails', () => { - // Given - spyOn(CollectionsUtil, 'isStringWithContent').and.throwError(new Error('String validation fails')); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - const instance = new ExecutionsStatusCriteria('running'); - - // When - const res = instance.meetCriteria(dataJobExecutions); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual(dataJobExecutions); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `ExecutionsStatusCriteria: failed to deserialize Data Job Execution Statuses.` - ); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + const cStartTime = new Date(); + const cEndTime = new Date(cStartTime.getTime() + 120); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "", + status: DataJobExecutionStatus.SUCCEEDED, + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "", + status: DataJobExecutionStatus.RUNNING, + }, + { + id: "cJob", + startTimeFormatted: datePipe.transform( + cStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + cEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: cEndTime.toISOString(), + duration: "120", + jobVersion: "", + status: DataJobExecutionStatus.PLATFORM_ERROR, + }, + ]; + }); + + describe("Methods::", () => { + describe("|meetCriteria|", () => { + it("should verify will return Array with aJob and bJob", () => { + // Given + const instance = new ExecutionsStatusCriteria("succeeded,running"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); + }); + + it("should verify will return Array with cJob", () => { + // Given + const instance = new ExecutionsStatusCriteria("platform_error"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it("should verify will return empty Array", () => { + // Given + const instance = new ExecutionsStatusCriteria("user_error"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([]); + }); + + it("should verify will return Array with all Jobs when serialized status criteria is empty string", () => { + // Given + const instance = new ExecutionsStatusCriteria(""); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return Array with all Jobs when serialized status criteria is Nil", () => { + // Given + const instance = new ExecutionsStatusCriteria(null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return empty Array when Executions are Nil", () => { + // Given + const instance = new ExecutionsStatusCriteria("running"); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); + + it("should verify will return Array with all Jobs when serialized status deserialization fails", () => { + // Given + spyOn(CollectionsUtil, "isStringWithContent").and.throwError( + new Error("String validation fails"), + ); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + const instance = new ExecutionsStatusCriteria("running"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `ExecutionsStatusCriteria: failed to deserialize Data Job Execution Statuses.`, + ); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts index b7aa928ed5..701c1c7c55 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts @@ -3,51 +3,62 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; +import { CollectionsUtil, Criteria } from "@versatiledatakit/shared"; -import { DataJobExecutionStatus } from '../../../../../../../model'; +import { DataJobExecutionStatus } from "../../../../../../../model"; -import { GridDataJobExecution } from '../../../model'; +import { GridDataJobExecution } from "../../../model"; /** * ** Executions Status filter criteria. */ export class ExecutionsStatusCriteria implements Criteria { - private readonly _dataJobExecutionStatuses: DataJobExecutionStatus[]; + private readonly _dataJobExecutionStatuses: DataJobExecutionStatus[]; - /** - * ** Constructor. - */ - constructor(dataJobExecutionStatusesSerialized: string) { - this._dataJobExecutionStatuses = ExecutionsStatusCriteria._deserializeExecutionStatuses(dataJobExecutionStatusesSerialized); - } + /** + * ** Constructor. + */ + constructor(dataJobExecutionStatusesSerialized: string) { + this._dataJobExecutionStatuses = + ExecutionsStatusCriteria._deserializeExecutionStatuses( + dataJobExecutionStatusesSerialized, + ); + } - /** - * @inheritDoc - */ - meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { - return [...(executions ?? [])].filter((execution) => { - const status = execution.status; + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + const status = execution.status; - if (this._dataJobExecutionStatuses.length === 0) { - return true; - } + if (this._dataJobExecutionStatuses.length === 0) { + return true; + } - return this._dataJobExecutionStatuses.includes(status); - }); - } + return this._dataJobExecutionStatuses.includes(status); + }); + } - private static _deserializeExecutionStatuses(dataJobExecutionStatusesSerialized: string): DataJobExecutionStatus[] { - try { - if (!CollectionsUtil.isStringWithContent(dataJobExecutionStatusesSerialized)) { - return []; - } + private static _deserializeExecutionStatuses( + dataJobExecutionStatusesSerialized: string, + ): DataJobExecutionStatus[] { + try { + if ( + !CollectionsUtil.isStringWithContent(dataJobExecutionStatusesSerialized) + ) { + return []; + } - return dataJobExecutionStatusesSerialized.toUpperCase().split(',') as DataJobExecutionStatus[]; - } catch (e) { - console.error(`ExecutionsStatusCriteria: failed to deserialize Data Job Execution Statuses.`); + return dataJobExecutionStatusesSerialized + .toUpperCase() + .split(",") as DataJobExecutionStatus[]; + } catch (e) { + console.error( + `ExecutionsStatusCriteria: failed to deserialize Data Job Execution Statuses.`, + ); - return []; - } + return []; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts index fb7bb28a32..e58a0238cf 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './executions-status.criteria'; +export * from "./executions-status.criteria"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts index 8d06749177..4e40c719b6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts @@ -3,141 +3,169 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionsStringCriteria } from './executions-string.criteria'; - -describe('ExecutionsStringCriteria', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const aStartTime = new Date(); - const aEndTime = new Date(aStartTime.getTime() + 100); - const bStartTime = new Date(); - const bEndTime = new Date(bStartTime.getTime() + 110); - const cStartTime = new Date(); - const cEndTime = new Date(cStartTime.getTime() + 120); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: 'aJob-10', - status: DataJobExecutionStatus.SUCCEEDED, - type: DataJobExecutionType.SCHEDULED, - opId: null - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: 'bJob-11', - status: DataJobExecutionStatus.RUNNING, - type: DataJobExecutionType.SCHEDULED, - opId: null - }, - { - id: 'cJob', - startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: cStartTime.toISOString(), - endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: cEndTime.toISOString(), - duration: '120', - jobVersion: 'cJob-12', - status: DataJobExecutionStatus.PLATFORM_ERROR, - type: DataJobExecutionType.MANUAL, - opId: 'cJob_opId' - } - ]; - }); - - describe('Methods::', () => { - describe('|meetCriteria|', () => { - it('should verify will return Array with aJob, bJob and cJob because match is partial and case insensitive', () => { - // Given - const instance = new ExecutionsStringCriteria('jobVersion', 'job'); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual(dataJobExecutions); - }); - - it('should verify will return Array with cJob', () => { - // Given - const instance = new ExecutionsStringCriteria('endTime', dataJobExecutions[2].endTime); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual([dataJobExecutions[2]]); - }); +import { DatePipe } from "@angular/common"; - it('should verify will return empty Array', () => { - // Given - const instance = new ExecutionsStringCriteria('id', 'aJobExecuted'); +import { TestBed } from "@angular/core/testing"; - // When - const res = instance.meetCriteria(dataJobExecutions); +import { + DATA_PIPELINES_DATE_TIME_FORMAT, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../../../../../../model"; - // Then - expect(res).toEqual([]); - }); +import { GridDataJobExecution } from "../../../model"; - it('should verify will return only cJob because other opId are Nil', () => { - // Given - const instance = new ExecutionsStringCriteria('opId', 'opid'); +import { ExecutionsStringCriteria } from "./executions-string.criteria"; - // When - const res = instance.meetCriteria(dataJobExecutions); +describe("ExecutionsStringCriteria", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // Then - expect(res).toEqual([dataJobExecutions[2]]); - }); - - it('should verify will return Array with all Jobs when search value (query) is Nil', () => { - // Given - const instance = new ExecutionsStringCriteria('id', null); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual(dataJobExecutions); - }); - - it('should verify will return empty Array when Executions are Nil', () => { - // Given - const instance = new ExecutionsStringCriteria('startTimeFormatted', dataJobExecutions[0].startTimeFormatted); - - // When - const res = instance.meetCriteria(null); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual([]); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + const cStartTime = new Date(); + const cEndTime = new Date(cStartTime.getTime() + 120); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "aJob-10", + status: DataJobExecutionStatus.SUCCEEDED, + type: DataJobExecutionType.SCHEDULED, + opId: null, + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "bJob-11", + status: DataJobExecutionStatus.RUNNING, + type: DataJobExecutionType.SCHEDULED, + opId: null, + }, + { + id: "cJob", + startTimeFormatted: datePipe.transform( + cStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + cEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: cEndTime.toISOString(), + duration: "120", + jobVersion: "cJob-12", + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL, + opId: "cJob_opId", + }, + ]; + }); + + describe("Methods::", () => { + describe("|meetCriteria|", () => { + it("should verify will return Array with aJob, bJob and cJob because match is partial and case insensitive", () => { + // Given + const instance = new ExecutionsStringCriteria("jobVersion", "job"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return Array with cJob", () => { + // Given + const instance = new ExecutionsStringCriteria( + "endTime", + dataJobExecutions[2].endTime, + ); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it("should verify will return empty Array", () => { + // Given + const instance = new ExecutionsStringCriteria("id", "aJobExecuted"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([]); + }); + + it("should verify will return only cJob because other opId are Nil", () => { + // Given + const instance = new ExecutionsStringCriteria("opId", "opid"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it("should verify will return Array with all Jobs when search value (query) is Nil", () => { + // Given + const instance = new ExecutionsStringCriteria("id", null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return empty Array when Executions are Nil", () => { + // Given + const instance = new ExecutionsStringCriteria( + "startTimeFormatted", + dataJobExecutions[0].startTimeFormatted, + ); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts index a3b0175280..4c957cc632 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts @@ -3,46 +3,55 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { get } from 'lodash'; +import { get } from "lodash"; -import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; +import { CollectionsUtil, Criteria } from "@versatiledatakit/shared"; -import { GridDataJobExecution } from '../../../model'; +import { GridDataJobExecution } from "../../../model"; /** * ** Executions Generic string filter criteria. */ export class ExecutionsStringCriteria implements Criteria { - private readonly _property: keyof GridDataJobExecution; - private readonly _searchValue: GridDataJobExecution[Exclude]; - - /** - * ** Constructor. - */ - constructor( - property: keyof GridDataJobExecution, - searchValue: GridDataJobExecution[Exclude] - ) { - this._property = property; - this._searchValue = searchValue; - } - - /** - * @inheritDoc - */ - meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { - return [...(executions ?? [])].filter((execution) => { - const value = get(execution, this._property); - - if (!CollectionsUtil.isString(this._searchValue)) { - return true; - } - - if (!CollectionsUtil.isString(value)) { - return false; - } - - return value.toLowerCase().includes(this._searchValue.toLowerCase()); - }); - } + private readonly _property: keyof GridDataJobExecution; + private readonly _searchValue: GridDataJobExecution[Exclude< + keyof GridDataJobExecution, + "deployment" + >]; + + /** + * ** Constructor. + */ + constructor( + property: keyof GridDataJobExecution, + searchValue: GridDataJobExecution[Exclude< + keyof GridDataJobExecution, + "deployment" + >], + ) { + this._property = property; + this._searchValue = searchValue; + } + + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + const value = get( + execution, + this._property, + ); + + if (!CollectionsUtil.isString(this._searchValue)) { + return true; + } + + if (!CollectionsUtil.isString(value)) { + return false; + } + + return value.toLowerCase().includes(this._searchValue.toLowerCase()); + }); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts index 51bfe65c6f..d032425afd 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './executions-string.criteria'; +export * from "./executions-string.criteria"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts index 86b3a35c56..deebba0f96 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts @@ -3,154 +3,180 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { CallFake, CollectionsUtil } from '@versatiledatakit/shared'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionsTypeCriteria } from './executions-type.criteria'; - -describe('ExecutionsTypeCriteria', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const aStartTime = new Date(); - const aEndTime = new Date(aStartTime.getTime() + 100); - const bStartTime = new Date(); - const bEndTime = new Date(bStartTime.getTime() + 110); - const cStartTime = new Date(); - const cEndTime = new Date(cStartTime.getTime() + 120); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: '', - status: DataJobExecutionStatus.SUCCEEDED, - type: DataJobExecutionType.SCHEDULED - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: '', - status: DataJobExecutionStatus.RUNNING, - type: DataJobExecutionType.SCHEDULED - }, - { - id: 'cJob', - startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: cStartTime.toISOString(), - endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: cEndTime.toISOString(), - duration: '120', - jobVersion: '', - status: DataJobExecutionStatus.PLATFORM_ERROR, - type: DataJobExecutionType.MANUAL - } - ]; - }); - - describe('Methods::', () => { - describe('|meetCriteria|', () => { - it('should verify will return Array with aJob and bJob', () => { - // Given - const instance = new ExecutionsTypeCriteria('scheduled'); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); - }); - - it('should verify will return Array with cJob', () => { - // Given - const instance = new ExecutionsTypeCriteria('manual'); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual([dataJobExecutions[2]]); - }); - - it('should verify will return empty Array', () => { - // Given - const instance = new ExecutionsTypeCriteria('unknown_type'); +import { DatePipe } from "@angular/common"; - // When - const res = instance.meetCriteria(dataJobExecutions); +import { TestBed } from "@angular/core/testing"; - // Then - expect(res).toEqual([]); - }); +import { CallFake, CollectionsUtil } from "@versatiledatakit/shared"; - it('should verify will return Array with all Jobs when serialized status criteria is empty string', () => { - // Given - const instance = new ExecutionsTypeCriteria(''); +import { + DATA_PIPELINES_DATE_TIME_FORMAT, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../../../../../../model"; - // When - const res = instance.meetCriteria(dataJobExecutions); +import { GridDataJobExecution } from "../../../model"; - // Then - expect(res).toEqual(dataJobExecutions); - }); +import { ExecutionsTypeCriteria } from "./executions-type.criteria"; - it('should verify will return Array with all Jobs when serialized status criteria is Nil', () => { - // Given - const instance = new ExecutionsTypeCriteria(null); +describe("ExecutionsTypeCriteria", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual(dataJobExecutions); - }); - - it('should verify will return empty Array when Executions are Nil', () => { - // Given - const instance = new ExecutionsTypeCriteria('scheduled'); - - // When - const res = instance.meetCriteria(null); - - // Then - expect(res).toEqual([]); - }); - - it('should verify will return Array with all Jobs when serialized status deserialization fails', () => { - // Given - spyOn(CollectionsUtil, 'isStringWithContent').and.throwError(new Error('String validation fails')); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - const instance = new ExecutionsTypeCriteria('scheduled'); - - // When - const res = instance.meetCriteria(dataJobExecutions); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual(dataJobExecutions); - expect(consoleErrorSpy).toHaveBeenCalledWith(`ExecutionsTypeCriteria: failed to deserialize Data Job Execution Types.`); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + const cStartTime = new Date(); + const cEndTime = new Date(cStartTime.getTime() + 120); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "", + status: DataJobExecutionStatus.SUCCEEDED, + type: DataJobExecutionType.SCHEDULED, + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "", + status: DataJobExecutionStatus.RUNNING, + type: DataJobExecutionType.SCHEDULED, + }, + { + id: "cJob", + startTimeFormatted: datePipe.transform( + cStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + cEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: cEndTime.toISOString(), + duration: "120", + jobVersion: "", + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL, + }, + ]; + }); + + describe("Methods::", () => { + describe("|meetCriteria|", () => { + it("should verify will return Array with aJob and bJob", () => { + // Given + const instance = new ExecutionsTypeCriteria("scheduled"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); + }); + + it("should verify will return Array with cJob", () => { + // Given + const instance = new ExecutionsTypeCriteria("manual"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it("should verify will return empty Array", () => { + // Given + const instance = new ExecutionsTypeCriteria("unknown_type"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([]); + }); + + it("should verify will return Array with all Jobs when serialized status criteria is empty string", () => { + // Given + const instance = new ExecutionsTypeCriteria(""); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return Array with all Jobs when serialized status criteria is Nil", () => { + // Given + const instance = new ExecutionsTypeCriteria(null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return empty Array when Executions are Nil", () => { + // Given + const instance = new ExecutionsTypeCriteria("scheduled"); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); + + it("should verify will return Array with all Jobs when serialized status deserialization fails", () => { + // Given + spyOn(CollectionsUtil, "isStringWithContent").and.throwError( + new Error("String validation fails"), + ); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + const instance = new ExecutionsTypeCriteria("scheduled"); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `ExecutionsTypeCriteria: failed to deserialize Data Job Execution Types.`, + ); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts index 6f694bc61f..289d133056 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts @@ -3,51 +3,62 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; +import { CollectionsUtil, Criteria } from "@versatiledatakit/shared"; -import { DataJobExecutionType } from '../../../../../../../model'; +import { DataJobExecutionType } from "../../../../../../../model"; -import { GridDataJobExecution } from '../../../model'; +import { GridDataJobExecution } from "../../../model"; /** * ** Executions Type filter criteria. */ export class ExecutionsTypeCriteria implements Criteria { - private readonly _dataJobExecutionTypes: DataJobExecutionType[]; + private readonly _dataJobExecutionTypes: DataJobExecutionType[]; - /** - * ** Constructor. - */ - constructor(dataJobExecutionTypesSerialized: string) { - this._dataJobExecutionTypes = ExecutionsTypeCriteria._deserializeExecutionTypes(dataJobExecutionTypesSerialized); - } + /** + * ** Constructor. + */ + constructor(dataJobExecutionTypesSerialized: string) { + this._dataJobExecutionTypes = + ExecutionsTypeCriteria._deserializeExecutionTypes( + dataJobExecutionTypesSerialized, + ); + } - /** - * @inheritDoc - */ - meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { - return [...(executions ?? [])].filter((execution) => { - const type = execution.type; + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + const type = execution.type; - if (this._dataJobExecutionTypes.length === 0) { - return true; - } + if (this._dataJobExecutionTypes.length === 0) { + return true; + } - return this._dataJobExecutionTypes.includes(type); - }); - } + return this._dataJobExecutionTypes.includes(type); + }); + } - private static _deserializeExecutionTypes(dataJobExecutionTypesSerialized: string): DataJobExecutionType[] { - try { - if (!CollectionsUtil.isStringWithContent(dataJobExecutionTypesSerialized)) { - return []; - } + private static _deserializeExecutionTypes( + dataJobExecutionTypesSerialized: string, + ): DataJobExecutionType[] { + try { + if ( + !CollectionsUtil.isStringWithContent(dataJobExecutionTypesSerialized) + ) { + return []; + } - return dataJobExecutionTypesSerialized.toUpperCase().split(',') as DataJobExecutionType[]; - } catch (e) { - console.error(`ExecutionsTypeCriteria: failed to deserialize Data Job Execution Types.`); + return dataJobExecutionTypesSerialized + .toUpperCase() + .split(",") as DataJobExecutionType[]; + } catch (e) { + console.error( + `ExecutionsTypeCriteria: failed to deserialize Data Job Execution Types.`, + ); - return []; - } + return []; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts index 095962082a..bc35ac389e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './executions-type.criteria'; +export * from "./executions-type.criteria"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html index 2168cf7204..29be278431 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html @@ -4,170 +4,170 @@ --> - We couldn't find any executions! + We couldn't find any executions! - Status - - - - - Type - - - - - Duration - - Start (UTC) - - End (UTC) - - ID - - Version - - Logs + Status + + + + + Type + + + + + Duration + + Start (UTC) + + End (UTC) + + ID + + Version + + Logs - + + + + + + + {{ + jobExecution.duration + }} + {{ + jobExecution.startTimeFormatted + }} + {{ + jobExecution.endTimeFormatted + }} + {{ + jobExecution.id + }} + {{ + jobExecution.jobVersion | slice: 0 : 8 + }} + - - - - - - - {{ jobExecution.duration }} - {{ jobExecution.startTimeFormatted }} - {{ jobExecution.endTimeFormatted }} - {{ jobExecution.id }} - {{ jobExecution.jobVersion | slice : 0 : 8 }} - - - - - - - - - + + + + + + + + - - - Executions per page - {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of {{ - pagination.totalItems }} executions - - + + + Executions per page + {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of + {{ pagination.totalItems }} executions + + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.scss index afb3567e57..328cca9760 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.scss @@ -4,40 +4,40 @@ */ .execution-type-cell { - text-align: center; + text-align: center; } .grid-column__min-width--s { - min-width: 3.5rem; + min-width: 3.5rem; } .grid-column__max-width--s { - max-width: 4rem; + max-width: 4rem; } clr-datagrid { - &.data-pipelines-job-executions-datagrid { - clr-dg-row { - &.data-pipelines-job__execution--highlighted { - --execution-row-highlighted: var(--clr-datagrid-row-hover, #e8e8e8); - background-color: var(--execution-row-highlighted); + &.data-pipelines-job-executions-datagrid { + clr-dg-row { + &.data-pipelines-job__execution--highlighted { + --execution-row-highlighted: var(--clr-datagrid-row-hover, #e8e8e8); + background-color: var(--execution-row-highlighted); - ::ng-deep .datagrid-row-sticky { - background-color: var(--execution-row-highlighted); - } - } + ::ng-deep .datagrid-row-sticky { + background-color: var(--execution-row-highlighted); } + } } + } } ::ng-deep .fade-to-dark.dark { - clr-datagrid { - &.data-pipelines-job-executions-datagrid { - clr-dg-row { - &.data-pipelines-job__execution--highlighted { - --execution-row-highlighted: #28404d; - } - } + clr-datagrid { + &.data-pipelines-job-executions-datagrid { + clr-dg-row { + &.data-pipelines-job__execution--highlighted { + --execution-row-highlighted: #28404d; } + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.ts index 9288f6b766..c74f7c8c73 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.ts @@ -4,458 +4,564 @@ */ import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - HostBinding, - Input, - OnChanges, - OnDestroy, - OnInit, - Output, - SimpleChanges -} from '@angular/core'; + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, +} from "@angular/core"; + +import { ClrDatagridSortOrder, ClrDatagridStateInterface } from "@clr/angular"; -import { ClrDatagridSortOrder, ClrDatagridStateInterface } from '@clr/angular'; +import { + AndCriteria, + CollectionsUtil, + Comparator, + Criteria, +} from "@versatiledatakit/shared"; -import { AndCriteria, CollectionsUtil, Comparator, Criteria } from '@versatiledatakit/shared'; +import { + FilterSortMutationObserver, + FiltersSortManager, +} from "../../../../../commons"; -import { FilterSortMutationObserver, FiltersSortManager } from '../../../../../commons'; +import { DataJobDeployment } from "../../../../../model"; -import { DataJobDeployment } from '../../../../../model'; +import { + ExecutionsFilterCriteria, + ExecutionsFilterPairs, + ExecutionsGridFilter, + ExecutionsSortCriteria, + ExecutionsSortPairs, + FILTER_DURATION_KEY, + FILTER_END_TIME_KEY, + FILTER_ID_KEY, + FILTER_START_TIME_KEY, + FILTER_STATUS_KEY, + FILTER_TYPE_KEY, + FILTER_VERSION_KEY, + GridDataJobExecution, + SORT_DURATION_KEY, + SORT_END_TIME_KEY, + SORT_ID_KEY, + SORT_START_TIME_KEY, + SORT_STATUS_KEY, + SORT_TYPE_KEY, + SORT_VERSION_KEY, +} from "../model"; import { - ExecutionsFilterCriteria, - ExecutionsFilterPairs, - ExecutionsGridFilter, - ExecutionsSortCriteria, - ExecutionsSortPairs, - FILTER_DURATION_KEY, - FILTER_END_TIME_KEY, - FILTER_ID_KEY, - FILTER_START_TIME_KEY, - FILTER_STATUS_KEY, - FILTER_TYPE_KEY, - FILTER_VERSION_KEY, - GridDataJobExecution, - SORT_DURATION_KEY, - SORT_END_TIME_KEY, - SORT_ID_KEY, - SORT_START_TIME_KEY, - SORT_STATUS_KEY, - SORT_TYPE_KEY, - SORT_VERSION_KEY -} from '../model'; - -import { ExecutionsStatusCriteria, ExecutionsStringCriteria, ExecutionsTypeCriteria } from './criteria'; -import { ExecutionDateComparator, ExecutionDefaultComparator, ExecutionDurationComparator } from './comparators'; + ExecutionsStatusCriteria, + ExecutionsStringCriteria, + ExecutionsTypeCriteria, +} from "./criteria"; +import { + ExecutionDateComparator, + ExecutionDefaultComparator, + ExecutionDurationComparator, +} from "./comparators"; /** * ** Supported filter criteria from Executions grid. */ -const GRID_SUPPORTED_EXECUTIONS_FILTER_KEY: Array = [ +const GRID_SUPPORTED_EXECUTIONS_FILTER_KEY: Array = + [ FILTER_STATUS_KEY, FILTER_TYPE_KEY, FILTER_DURATION_KEY, FILTER_START_TIME_KEY, FILTER_END_TIME_KEY, FILTER_ID_KEY, - FILTER_VERSION_KEY -]; + FILTER_VERSION_KEY, + ]; /** * ** Supported sort criteria from Executions grid. */ const GRID_SUPPORTED_EXECUTIONS_SORT_KEY: Array = [ - SORT_STATUS_KEY, - SORT_TYPE_KEY, - SORT_DURATION_KEY, - SORT_START_TIME_KEY, - SORT_END_TIME_KEY, - SORT_ID_KEY, - SORT_VERSION_KEY + SORT_STATUS_KEY, + SORT_TYPE_KEY, + SORT_DURATION_KEY, + SORT_START_TIME_KEY, + SORT_END_TIME_KEY, + SORT_ID_KEY, + SORT_VERSION_KEY, ]; -type GridExecutionFilterCriteria = Exclude; -type GridExecutionsFilterPairs = ExecutionsFilterPairs; +type GridExecutionFilterCriteria = Exclude< + ExecutionsFilterCriteria, + "timePeriod" +>; +type GridExecutionsFilterPairs = + ExecutionsFilterPairs; -type GridExecutionSortCriteria = Exclude; +type GridExecutionSortCriteria = Exclude; type GridExecutionsSortPairs = ExecutionsFilterPairs; type GridStateLocal = { - filter: GridExecutionsFilterPairs[]; - sort: ExecutionsSortPairs; + filter: GridExecutionsFilterPairs[]; + sort: ExecutionsSortPairs; }; export interface GridCriteriaAndComparator { - filter: Criteria; - sort: Comparator; + filter: Criteria; + sort: Comparator; } @Component({ - selector: 'lib-data-job-executions-grid', - templateUrl: './data-job-executions-grid.component.html', - styleUrls: ['./data-job-executions-grid.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: "lib-data-job-executions-grid", + templateUrl: "./data-job-executions-grid.component.html", + styleUrls: ["./data-job-executions-grid.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DataJobExecutionsGridComponent implements OnChanges, OnInit, OnDestroy { - @Input() jobExecutions: GridDataJobExecution[]; - @Input() loading = false; - - /** - * ** Executions filters sort manager injected from parent. - */ - @Input() filtersSortManager: Readonly< - FiltersSortManager - >; - - /** - * ** If provided will try to highlight row where execution id will match. - */ - @Input() highlightedExecutionId: string; - - /** - * ** Event Emitter that emits events on every user action on grid filters or sort. - */ - @Output() gridCriteriaAndComparatorChanged: EventEmitter = new EventEmitter(); - - @HostBinding('attr.data-cy') public readonly attributeDataCy = 'data-pipelines-data-job-executions'; - - openDeploymentDetailsModal = false; - jobDeploymentModalData: DataJobDeployment; - - paginatedJobExecutions: GridDataJobExecution[] = []; - gridState: ClrDatagridStateInterface; - - paginationPageNumber: number; - paginationPageSize: number; - paginationTotalItems: number; - - isInitialCriteriasEmit = true; - - private _appliedGridState: GridStateLocal = { - filter: [], - sort: undefined - }; - private _previousAppliedGridState: GridStateLocal = { - filter: [], - sort: undefined - }; - - private _filterMutationObserver: FilterSortMutationObserver< - ExecutionsFilterCriteria, - string, - ExecutionsSortCriteria, - ClrDatagridSortOrder - >; - - /** - * ** Reference to scheduled timeout for emitting Grid Criteria and Comparator. - * @private - */ - private _gridCriteriaAndComparatorEmitterTimeoutRef: number; - - /** - * ** Constructor. - */ - constructor(private readonly changeDetectorRef: ChangeDetectorRef) {} - - /** - * ** NgFor elements tracking function. - */ - trackByFn(index: number, execution: GridDataJobExecution): string { - return `${index}|${execution?.id}`; +export class DataJobExecutionsGridComponent + implements OnChanges, OnInit, OnDestroy +{ + @Input() jobExecutions: GridDataJobExecution[]; + @Input() loading = false; + + /** + * ** Executions filters sort manager injected from parent. + */ + @Input() filtersSortManager: Readonly< + FiltersSortManager< + ExecutionsFilterCriteria, + string, + ExecutionsSortCriteria, + ClrDatagridSortOrder + > + >; + + /** + * ** If provided will try to highlight row where execution id will match. + */ + @Input() highlightedExecutionId: string; + + /** + * ** Event Emitter that emits events on every user action on grid filters or sort. + */ + @Output() + gridCriteriaAndComparatorChanged: EventEmitter = + new EventEmitter(); + + @HostBinding("attr.data-cy") public readonly attributeDataCy = + "data-pipelines-data-job-executions"; + + openDeploymentDetailsModal = false; + jobDeploymentModalData: DataJobDeployment; + + paginatedJobExecutions: GridDataJobExecution[] = []; + gridState: ClrDatagridStateInterface; + + paginationPageNumber: number; + paginationPageSize: number; + paginationTotalItems: number; + + isInitialCriteriasEmit = true; + + private _appliedGridState: GridStateLocal = { + filter: [], + sort: undefined, + }; + private _previousAppliedGridState: GridStateLocal = { + filter: [], + sort: undefined, + }; + + private _filterMutationObserver: FilterSortMutationObserver< + ExecutionsFilterCriteria, + string, + ExecutionsSortCriteria, + ClrDatagridSortOrder + >; + + /** + * ** Reference to scheduled timeout for emitting Grid Criteria and Comparator. + * @private + */ + private _gridCriteriaAndComparatorEmitterTimeoutRef: number; + + /** + * ** Constructor. + */ + constructor(private readonly changeDetectorRef: ChangeDetectorRef) {} + + /** + * ** NgFor elements tracking function. + */ + trackByFn(index: number, execution: GridDataJobExecution): string { + return `${index}|${execution?.id}`; + } + + showDeploymentDetails(jobExecution: GridDataJobExecution) { + this.openDeploymentDetailsModal = true; + this.jobDeploymentModalData = jobExecution.deployment; + + this.changeDetectorRef.detectChanges(); + } + + /** + * ** Main callback (listener) for ClrGrid state mutation, like filters, sort. + */ + gridRefresh(state: ClrDatagridStateInterface): void { + if (!state) { + return; } - showDeploymentDetails(jobExecution: GridDataJobExecution) { - this.openDeploymentDetailsModal = true; - this.jobDeploymentModalData = jobExecution.deployment; + let skipCriteriaAndComparatorEmitterDebouncing = false; + this.gridState = state; - this.changeDetectorRef.detectChanges(); + if (this.isInitialCriteriasEmit) { + this.isInitialCriteriasEmit = false; + skipCriteriaAndComparatorEmitterDebouncing = true; } - /** - * ** Main callback (listener) for ClrGrid state mutation, like filters, sort. - */ - gridRefresh(state: ClrDatagridStateInterface): void { - if (!state) { - return; - } - - let skipCriteriaAndComparatorEmitterDebouncing = false; - this.gridState = state; - - if (this.isInitialCriteriasEmit) { - this.isInitialCriteriasEmit = false; - skipCriteriaAndComparatorEmitterDebouncing = true; - } - - this._populateManagerFilters(state); - this._populateManagerSort(state); - this._evaluateGridStateMutation(skipCriteriaAndComparatorEmitterDebouncing); - - this._paginateExecutions(state); - - // update Browser URL once only, for every Grid event - this.filtersSortManager.updateBrowserUrl(); + this._populateManagerFilters(state); + this._populateManagerSort(state); + this._evaluateGridStateMutation(skipCriteriaAndComparatorEmitterDebouncing); + + this._paginateExecutions(state); + + // update Browser URL once only, for every Grid event + this.filtersSortManager.updateBrowserUrl(); + } + + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + if ( + changes["jobExecutions"] && + !CollectionsUtil.isEqual( + changes["jobExecutions"].previousValue, + changes["jobExecutions"].currentValue, + ) + ) { + this.paginationTotalItems = this.jobExecutions.length; + this._paginateExecutions(this.gridState); } + } + + /** + * @inheritDoc + */ + ngOnInit(): void { + this._filterMutationObserver = (changes) => { + if ( + changes.some(([key]: GridExecutionsSortPairs) => + [ + ...GRID_SUPPORTED_EXECUTIONS_FILTER_KEY, + ...GRID_SUPPORTED_EXECUTIONS_SORT_KEY, + ].includes(key), + ) + ) { + this.changeDetectorRef.markForCheck(); + } + }; - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges): void { - if ( - changes['jobExecutions'] && - !CollectionsUtil.isEqual(changes['jobExecutions'].previousValue, changes['jobExecutions'].currentValue) - ) { - this.paginationTotalItems = this.jobExecutions.length; - this._paginateExecutions(this.gridState); - } + // register callback that would listen for mutation of supported filter and sort criteria + this.filtersSortManager.registerMutationObserver( + this._filterMutationObserver, + ); + } + + /** + * @inheritDoc + */ + ngOnDestroy(): void { + if ( + CollectionsUtil.isNumber(this._gridCriteriaAndComparatorEmitterTimeoutRef) + ) { + clearTimeout(this._gridCriteriaAndComparatorEmitterTimeoutRef); } - /** - * @inheritDoc - */ - ngOnInit(): void { - this._filterMutationObserver = (changes) => { - if ( - changes.some(([key]: GridExecutionsSortPairs) => - [...GRID_SUPPORTED_EXECUTIONS_FILTER_KEY, ...GRID_SUPPORTED_EXECUTIONS_SORT_KEY].includes(key) - ) - ) { - this.changeDetectorRef.markForCheck(); - } - }; - - // register callback that would listen for mutation of supported filter and sort criteria - this.filtersSortManager.registerMutationObserver(this._filterMutationObserver); + this.filtersSortManager.deleteMutationObserver( + this._filterMutationObserver, + ); + } + + /** + * ** Extract filters from grid state. + * - use bulk operation to update manager + * @private + */ + private _populateManagerFilters(state: ClrDatagridStateInterface): void { + // on every grid emitted event save currently applied filters for comparison + this._previousAppliedGridState.filter = [...this._appliedGridState.filter]; + + // when grid has user applied filters + if (CollectionsUtil.isArray(state.filters)) { + if (state.filters.length > 0) { + const newFilterPairs: GridExecutionsFilterPairs[] = state.filters.map( + (filter: ExecutionsGridFilter) => + [filter.property, filter.value] as GridExecutionsFilterPairs, + ); + + // remove known filters if they are already set in the manager but are missing from grid state + const filtersForDeletion: GridExecutionsFilterPairs[] = + GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter( + (supportedCriteria) => + this.filtersSortManager.hasFilter(supportedCriteria) && + newFilterPairs.findIndex( + ([criteria]) => supportedCriteria === criteria, + ) === -1, + ).map((supportedCriteria) => [supportedCriteria, null]); + + newFilterPairs.push(...filtersForDeletion); + + this.filtersSortManager.bulkUpdate( + newFilterPairs.map(([criteria, value]) => [ + criteria, + value, + "filter", + ]), + ); + + // set new filters to applied grid filters state + this._appliedGridState.filter = [...newFilterPairs]; + + return; + } + } else { + // clear applied grid filters state + this._appliedGridState.filter = []; } - /** - * @inheritDoc - */ - ngOnDestroy(): void { - if (CollectionsUtil.isNumber(this._gridCriteriaAndComparatorEmitterTimeoutRef)) { - clearTimeout(this._gridCriteriaAndComparatorEmitterTimeoutRef); - } - - this.filtersSortManager.deleteMutationObserver(this._filterMutationObserver); + // when grid doesn't have user applied filters but manager has from previous actions + if (this.filtersSortManager.hasAnyFilter()) { + // remove known filters if they are already set in the manager + const filtersForDeletion = GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter( + (criteria) => this.filtersSortManager.hasFilter(criteria), + ).map((criteria) => [criteria, null] as GridExecutionsFilterPairs); + + if (filtersForDeletion.length > 0) { + this.filtersSortManager.bulkUpdate( + filtersForDeletion.map(([criteria, value]) => [ + criteria, + value, + "filter", + ]), + ); + } } - - /** - * ** Extract filters from grid state. - * - use bulk operation to update manager - * @private - */ - private _populateManagerFilters(state: ClrDatagridStateInterface): void { - // on every grid emitted event save currently applied filters for comparison - this._previousAppliedGridState.filter = [...this._appliedGridState.filter]; - - // when grid has user applied filters - if (CollectionsUtil.isArray(state.filters)) { - if (state.filters.length > 0) { - const newFilterPairs: GridExecutionsFilterPairs[] = state.filters.map( - (filter: ExecutionsGridFilter) => [filter.property, filter.value] as GridExecutionsFilterPairs - ); - - // remove known filters if they are already set in the manager but are missing from grid state - const filtersForDeletion: GridExecutionsFilterPairs[] = GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter( - (supportedCriteria) => - this.filtersSortManager.hasFilter(supportedCriteria) && - newFilterPairs.findIndex(([criteria]) => supportedCriteria === criteria) === -1 - ).map((supportedCriteria) => [supportedCriteria, null]); - - newFilterPairs.push(...filtersForDeletion); - - this.filtersSortManager.bulkUpdate(newFilterPairs.map(([criteria, value]) => [criteria, value, 'filter'])); - - // set new filters to applied grid filters state - this._appliedGridState.filter = [...newFilterPairs]; - - return; - } - } else { - // clear applied grid filters state - this._appliedGridState.filter = []; - } - - // when grid doesn't have user applied filters but manager has from previous actions - if (this.filtersSortManager.hasAnyFilter()) { - // remove known filters if they are already set in the manager - const filtersForDeletion = GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter((criteria) => - this.filtersSortManager.hasFilter(criteria) - ).map((criteria) => [criteria, null] as GridExecutionsFilterPairs); - - if (filtersForDeletion.length > 0) { - this.filtersSortManager.bulkUpdate(filtersForDeletion.map(([criteria, value]) => [criteria, value, 'filter'])); - } - } + } + + /** + * ** Extract sort criteria and direction from grid state and update the manager + * @private + */ + private _populateManagerSort(state: ClrDatagridStateInterface): void { + // on every grid emitted event save currently applied sort pair + this._previousAppliedGridState.sort = this._appliedGridState.sort; + + // when grid has user applied sort + if (CollectionsUtil.isDefined(state.sort)) { + const property: ExecutionsSortCriteria = + CollectionsUtil.isStringWithContent(state.sort.by) + ? (state.sort.by as ExecutionsSortCriteria) + : (state.sort.by as unknown as { property: ExecutionsSortCriteria }) + ?.property; + const direction = state.sort.reverse + ? ClrDatagridSortOrder.DESC + : ClrDatagridSortOrder.ASC; + const newSortPairs: ExecutionsSortPairs[] = [[property, direction]]; + + // always remove known previous stored sort criteria and direction + // manager supports multi sort, but grid support single sort only + // remove known sorts if they are already set in the manager but are missing from grid state + const sortsForDeletion: ExecutionsSortPairs[] = + GRID_SUPPORTED_EXECUTIONS_SORT_KEY.filter( + (supportedCriteria) => + this.filtersSortManager.hasSort(supportedCriteria) && + newSortPairs.findIndex( + ([criteria]) => supportedCriteria === criteria, + ) === -1, + ).map((supportedCriteria) => [supportedCriteria, null]); + + newSortPairs.push(...sortsForDeletion); + + this.filtersSortManager.bulkUpdate( + newSortPairs.map(([criteria, value]) => [criteria, value, "sort"]), + ); + + // set new sort to applied grid sort state + this._appliedGridState.sort = newSortPairs[0]; + + return; + } else { + // clear applied grid sort state + this._appliedGridState.sort = undefined; } - /** - * ** Extract sort criteria and direction from grid state and update the manager - * @private - */ - private _populateManagerSort(state: ClrDatagridStateInterface): void { - // on every grid emitted event save currently applied sort pair - this._previousAppliedGridState.sort = this._appliedGridState.sort; - - // when grid has user applied sort - if (CollectionsUtil.isDefined(state.sort)) { - const property: ExecutionsSortCriteria = CollectionsUtil.isStringWithContent(state.sort.by) - ? (state.sort.by as ExecutionsSortCriteria) - : (state.sort.by as unknown as { property: ExecutionsSortCriteria })?.property; - const direction = state.sort.reverse ? ClrDatagridSortOrder.DESC : ClrDatagridSortOrder.ASC; - const newSortPairs: ExecutionsSortPairs[] = [[property, direction]]; - - // always remove known previous stored sort criteria and direction - // manager supports multi sort, but grid support single sort only - // remove known sorts if they are already set in the manager but are missing from grid state - const sortsForDeletion: ExecutionsSortPairs[] = GRID_SUPPORTED_EXECUTIONS_SORT_KEY.filter( - (supportedCriteria) => - this.filtersSortManager.hasSort(supportedCriteria) && - newSortPairs.findIndex(([criteria]) => supportedCriteria === criteria) === -1 - ).map((supportedCriteria) => [supportedCriteria, null]); - - newSortPairs.push(...sortsForDeletion); - - this.filtersSortManager.bulkUpdate(newSortPairs.map(([criteria, value]) => [criteria, value, 'sort'])); - - // set new sort to applied grid sort state - this._appliedGridState.sort = newSortPairs[0]; - - return; - } else { - // clear applied grid sort state - this._appliedGridState.sort = undefined; - } - - // when grid doesn't have user applied sort but manager has from previous actions - if (this.filtersSortManager.hasAnySort()) { - // remove known sort if they are already set in the manager - const sortsForDeletion = GRID_SUPPORTED_EXECUTIONS_SORT_KEY.filter((criteria) => this.filtersSortManager.hasSort(criteria)).map( - (criteria) => [criteria, null] as ExecutionsSortPairs - ); - - if (sortsForDeletion.length > 0) { - this.filtersSortManager.bulkUpdate(sortsForDeletion.map(([criteria, value]) => [criteria, value, 'sort'])); - } - } + // when grid doesn't have user applied sort but manager has from previous actions + if (this.filtersSortManager.hasAnySort()) { + // remove known sort if they are already set in the manager + const sortsForDeletion = GRID_SUPPORTED_EXECUTIONS_SORT_KEY.filter( + (criteria) => this.filtersSortManager.hasSort(criteria), + ).map((criteria) => [criteria, null] as ExecutionsSortPairs); + + if (sortsForDeletion.length > 0) { + this.filtersSortManager.bulkUpdate( + sortsForDeletion.map(([criteria, value]) => [ + criteria, + value, + "sort", + ]), + ); + } } - - private _paginateExecutions(state: ClrDatagridStateInterface): void { - this.paginationPageNumber = state?.page?.current ?? 1; - this.paginationPageSize = state?.page?.size ?? 10; - - const pageSize = CollectionsUtil.isDefined(this.paginationPageSize) ? this.paginationPageSize : 10; - const pageNumber = CollectionsUtil.isDefined(this.paginationPageNumber) ? this.paginationPageNumber - 1 : 0; - const from = pageNumber * pageSize; - const to = (pageNumber + 1) * pageSize; - - this.paginatedJobExecutions = this.jobExecutions.slice(from, to); + } + + private _paginateExecutions(state: ClrDatagridStateInterface): void { + this.paginationPageNumber = state?.page?.current ?? 1; + this.paginationPageSize = state?.page?.size ?? 10; + + const pageSize = CollectionsUtil.isDefined(this.paginationPageSize) + ? this.paginationPageSize + : 10; + const pageNumber = CollectionsUtil.isDefined(this.paginationPageNumber) + ? this.paginationPageNumber - 1 + : 0; + const from = pageNumber * pageSize; + const to = (pageNumber + 1) * pageSize; + + this.paginatedJobExecutions = this.jobExecutions.slice(from, to); + } + + private _evaluateGridStateMutation(skipDebouncing = false): void { + if ( + this._previousAppliedGridState.filter.length !== + this._appliedGridState.filter.length || + this._previousAppliedGridState.sort !== this._appliedGridState.sort + ) { + this._emitGridCriteriaAndComparator(skipDebouncing); + + return; } - private _evaluateGridStateMutation(skipDebouncing = false): void { - if ( - this._previousAppliedGridState.filter.length !== this._appliedGridState.filter.length || - this._previousAppliedGridState.sort !== this._appliedGridState.sort - ) { - this._emitGridCriteriaAndComparator(skipDebouncing); - - return; - } - - if (this._previousAppliedGridState.filter.length === this._appliedGridState.filter.length) { - if (!CollectionsUtil.isEqual(this._previousAppliedGridState.filter, this._appliedGridState.filter)) { - this._emitGridCriteriaAndComparator(skipDebouncing); - - return; - } - } - - if (!CollectionsUtil.isEqual(this._previousAppliedGridState.sort, this._appliedGridState.sort)) { - this._emitGridCriteriaAndComparator(skipDebouncing); - - return; - } + if ( + this._previousAppliedGridState.filter.length === + this._appliedGridState.filter.length + ) { + if ( + !CollectionsUtil.isEqual( + this._previousAppliedGridState.filter, + this._appliedGridState.filter, + ) + ) { + this._emitGridCriteriaAndComparator(skipDebouncing); + + return; + } } - private _emitGridCriteriaAndComparator(skipDebouncing = false): void { - if (CollectionsUtil.isNumber(this._gridCriteriaAndComparatorEmitterTimeoutRef)) { - clearTimeout(this._gridCriteriaAndComparatorEmitterTimeoutRef); - - this._gridCriteriaAndComparatorEmitterTimeoutRef = null; - } - - if (skipDebouncing) { - this.gridCriteriaAndComparatorChanged.emit({ - filter: this._createFilterCriteria(), - sort: this._createSortComparator() - }); + if ( + !CollectionsUtil.isEqual( + this._previousAppliedGridState.sort, + this._appliedGridState.sort, + ) + ) { + this._emitGridCriteriaAndComparator(skipDebouncing); - return; - } + return; + } + } - this._gridCriteriaAndComparatorEmitterTimeoutRef = setTimeout(() => { - this.gridCriteriaAndComparatorChanged.emit({ - filter: this._createFilterCriteria(), - sort: this._createSortComparator() - }); + private _emitGridCriteriaAndComparator(skipDebouncing = false): void { + if ( + CollectionsUtil.isNumber(this._gridCriteriaAndComparatorEmitterTimeoutRef) + ) { + clearTimeout(this._gridCriteriaAndComparatorEmitterTimeoutRef); - this._gridCriteriaAndComparatorEmitterTimeoutRef = null; - }, 200); + this._gridCriteriaAndComparatorEmitterTimeoutRef = null; } - private _createFilterCriteria(): Criteria { - const criteria: Criteria[] = []; - - for (const filterPair of this._appliedGridState.filter) { - if (filterPair[0] === 'status') { - criteria.push(new ExecutionsStatusCriteria(filterPair[1])); + if (skipDebouncing) { + this.gridCriteriaAndComparatorChanged.emit({ + filter: this._createFilterCriteria(), + sort: this._createSortComparator(), + }); - continue; - } + return; + } - if (filterPair[0] === 'type') { - criteria.push(new ExecutionsTypeCriteria(filterPair[1])); + this._gridCriteriaAndComparatorEmitterTimeoutRef = setTimeout(() => { + this.gridCriteriaAndComparatorChanged.emit({ + filter: this._createFilterCriteria(), + sort: this._createSortComparator(), + }); - continue; - } + this._gridCriteriaAndComparatorEmitterTimeoutRef = null; + }, 200); + } - if (filterPair[0] === 'startTime') { - criteria.push(new ExecutionsStringCriteria('startTimeFormatted', filterPair[1])); + private _createFilterCriteria(): Criteria { + const criteria: Criteria[] = []; - continue; - } + for (const filterPair of this._appliedGridState.filter) { + if (filterPair[0] === "status") { + criteria.push(new ExecutionsStatusCriteria(filterPair[1])); - if (filterPair[0] === 'endTime') { - criteria.push(new ExecutionsStringCriteria('endTimeFormatted', filterPair[1])); + continue; + } - continue; - } + if (filterPair[0] === "type") { + criteria.push(new ExecutionsTypeCriteria(filterPair[1])); - criteria.push(new ExecutionsStringCriteria(filterPair[0], filterPair[1])); - } + continue; + } - return criteria.length > 0 ? new AndCriteria(...criteria) : null; - } + if (filterPair[0] === "startTime") { + criteria.push( + new ExecutionsStringCriteria("startTimeFormatted", filterPair[1]), + ); - private _createSortComparator(): Comparator { - if (CollectionsUtil.isDefined(this._appliedGridState.sort)) { - const [sortCriteria, sortValue] = this._appliedGridState.sort; + continue; + } - if (sortCriteria === 'duration') { - return new ExecutionDurationComparator(sortValue === ClrDatagridSortOrder.ASC ? 'ASC' : 'DESC'); - } + if (filterPair[0] === "endTime") { + criteria.push( + new ExecutionsStringCriteria("endTimeFormatted", filterPair[1]), + ); - if (sortCriteria === 'startTime' || sortCriteria === 'endTime') { - return new ExecutionDateComparator(sortCriteria, sortValue === ClrDatagridSortOrder.ASC ? 'ASC' : 'DESC'); - } + continue; + } - return new ExecutionDefaultComparator(sortCriteria, sortValue === ClrDatagridSortOrder.ASC ? 'ASC' : 'DESC'); - } + criteria.push(new ExecutionsStringCriteria(filterPair[0], filterPair[1])); + } - return null; + return criteria.length > 0 ? new AndCriteria(...criteria) : null; + } + + private _createSortComparator(): Comparator { + if (CollectionsUtil.isDefined(this._appliedGridState.sort)) { + const [sortCriteria, sortValue] = this._appliedGridState.sort; + + if (sortCriteria === "duration") { + return new ExecutionDurationComparator( + sortValue === ClrDatagridSortOrder.ASC ? "ASC" : "DESC", + ); + } + + if (sortCriteria === "startTime" || sortCriteria === "endTime") { + return new ExecutionDateComparator( + sortCriteria, + sortValue === ClrDatagridSortOrder.ASC ? "ASC" : "DESC", + ); + } + + return new ExecutionDefaultComparator( + sortCriteria, + sortValue === ClrDatagridSortOrder.ASC ? "ASC" : "DESC", + ); } + + return null; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts index 5d9800f779..0c5995f577 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-executions-grid.component'; -export * from './comparators/duration/execution-duration.comparator'; +export * from "./data-job-executions-grid.component"; +export * from "./comparators/duration/execution-duration.comparator"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts index e39c464096..29672eeb1b 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts @@ -3,144 +3,180 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../../../model'; - -import { GridDataJobExecution } from '../../../model'; - -import { ExecutionsTimePeriodCriteria } from './executions-time-period.criteria'; - -describe('ExecutionsTimePeriodCriteria', () => { - let datePipe: DatePipe; - let dataJobExecutions: GridDataJobExecution[]; - - let aStartTime: Date; - let bStartTime: Date; - let cStartTime: Date; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - const currentTime = new Date(); - - aStartTime = new Date(currentTime.getTime() + 10); - const aEndTime = new Date(aStartTime.getTime() + 110); - bStartTime = new Date(currentTime.getTime() + 20); - const bEndTime = new Date(bStartTime.getTime() + 120); - cStartTime = new Date(currentTime.getTime() + 30); - const cEndTime = new Date(cStartTime.getTime() + 130); - - dataJobExecutions = [ - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: '', - status: DataJobExecutionStatus.SUCCEEDED, - type: DataJobExecutionType.SCHEDULED - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: '', - status: DataJobExecutionStatus.RUNNING, - type: DataJobExecutionType.SCHEDULED - }, - { - id: 'cJob', - startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: cStartTime.toISOString(), - endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: cEndTime.toISOString(), - duration: '120', - jobVersion: '', - status: DataJobExecutionStatus.PLATFORM_ERROR, - type: DataJobExecutionType.MANUAL - } - ]; - }); - - describe('Methods::', () => { - describe('|meetCriteria|', () => { - it('should verify will return Array with aJob and bJob', () => { - // Given - const instance = new ExecutionsTimePeriodCriteria(aStartTime, bStartTime); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); - }); - - it('should verify will return Array with cJob', () => { - // Given - const instance = new ExecutionsTimePeriodCriteria(cStartTime, cStartTime); - - // When - const res = instance.meetCriteria(dataJobExecutions); +import { DatePipe } from "@angular/common"; - // Then - expect(res).toEqual([dataJobExecutions[2]]); - }); +import { TestBed } from "@angular/core/testing"; - it('should verify will return empty Array when startTime is Nil', () => { - // Given - const instance = new ExecutionsTimePeriodCriteria(aStartTime, cStartTime); +import { + DATA_PIPELINES_DATE_TIME_FORMAT, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../../../../../../model"; - // When - const res = instance.meetCriteria(dataJobExecutions.map((ex) => ({ ...ex, startTime: null }))); +import { GridDataJobExecution } from "../../../model"; - // Then - expect(res).toEqual([]); - }); +import { ExecutionsTimePeriodCriteria } from "./executions-time-period.criteria"; - it('should verify will return Array with all Jobs when from time is Nil', () => { - // Given - const instance = new ExecutionsTimePeriodCriteria(null, cStartTime); +describe("ExecutionsTimePeriodCriteria", () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; - // When - const res = instance.meetCriteria(dataJobExecutions); + let aStartTime: Date; + let bStartTime: Date; + let cStartTime: Date; - // Then - expect(res).toEqual(dataJobExecutions); - }); - - it('should verify will return Array with all Jobs when to time is Nil', () => { - // Given - const instance = new ExecutionsTimePeriodCriteria(aStartTime, null); - - // When - const res = instance.meetCriteria(dataJobExecutions); - - // Then - expect(res).toEqual(dataJobExecutions); - }); - - it('should verify will return empty Array when Executions are Nil', () => { - // Given - const instance = new ExecutionsTimePeriodCriteria(aStartTime, cStartTime); - - // When - const res = instance.meetCriteria(null); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); - // Then - expect(res).toEqual([]); - }); - }); + datePipe = TestBed.inject(DatePipe); + + const currentTime = new Date(); + + aStartTime = new Date(currentTime.getTime() + 10); + const aEndTime = new Date(aStartTime.getTime() + 110); + bStartTime = new Date(currentTime.getTime() + 20); + const bEndTime = new Date(bStartTime.getTime() + 120); + cStartTime = new Date(currentTime.getTime() + 30); + const cEndTime = new Date(cStartTime.getTime() + 130); + + dataJobExecutions = [ + { + id: "aJob", + startTimeFormatted: datePipe.transform( + aStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + aEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: aEndTime.toISOString(), + duration: "100", + jobVersion: "", + status: DataJobExecutionStatus.SUCCEEDED, + type: DataJobExecutionType.SCHEDULED, + }, + { + id: "bJob", + startTimeFormatted: datePipe.transform( + bStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + bEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: bEndTime.toISOString(), + duration: "110", + jobVersion: "", + status: DataJobExecutionStatus.RUNNING, + type: DataJobExecutionType.SCHEDULED, + }, + { + id: "cJob", + startTimeFormatted: datePipe.transform( + cStartTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform( + cEndTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + ), + endTime: cEndTime.toISOString(), + duration: "120", + jobVersion: "", + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL, + }, + ]; + }); + + describe("Methods::", () => { + describe("|meetCriteria|", () => { + it("should verify will return Array with aJob and bJob", () => { + // Given + const instance = new ExecutionsTimePeriodCriteria( + aStartTime, + bStartTime, + ); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); + }); + + it("should verify will return Array with cJob", () => { + // Given + const instance = new ExecutionsTimePeriodCriteria( + cStartTime, + cStartTime, + ); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it("should verify will return empty Array when startTime is Nil", () => { + // Given + const instance = new ExecutionsTimePeriodCriteria( + aStartTime, + cStartTime, + ); + + // When + const res = instance.meetCriteria( + dataJobExecutions.map((ex) => ({ ...ex, startTime: null })), + ); + + // Then + expect(res).toEqual([]); + }); + + it("should verify will return Array with all Jobs when from time is Nil", () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(null, cStartTime); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return Array with all Jobs when to time is Nil", () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(aStartTime, null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it("should verify will return empty Array when Executions are Nil", () => { + // Given + const instance = new ExecutionsTimePeriodCriteria( + aStartTime, + cStartTime, + ); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts index 1ba07087d6..f9ed63fa97 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts @@ -3,41 +3,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; +import { CollectionsUtil, Criteria } from "@versatiledatakit/shared"; -import { GridDataJobExecution } from '../../../model'; +import { GridDataJobExecution } from "../../../model"; /** * ** Executions Time Period filter criteria. */ export class ExecutionsTimePeriodCriteria implements Criteria { - private readonly _fromDateTime: Date; - private readonly _toDateTime: Date; - - /** - * ** Constructor. - */ - constructor(fromDateTime: Date, toDateTime: Date) { - this._fromDateTime = fromDateTime; - this._toDateTime = toDateTime; - } - - /** - * @inheritDoc - */ - meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { - return [...(executions ?? [])].filter((execution) => { - if (CollectionsUtil.isNil(this._fromDateTime) || CollectionsUtil.isNil(this._toDateTime)) { - return true; - } - - if (!CollectionsUtil.isString(execution.startTime)) { - return false; - } - - const startTime = new Date(execution.startTime).getTime(); - - return this._fromDateTime.getTime() <= startTime && startTime <= this._toDateTime.getTime(); - }); - } + private readonly _fromDateTime: Date; + private readonly _toDateTime: Date; + + /** + * ** Constructor. + */ + constructor(fromDateTime: Date, toDateTime: Date) { + this._fromDateTime = fromDateTime; + this._toDateTime = toDateTime; + } + + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + if ( + CollectionsUtil.isNil(this._fromDateTime) || + CollectionsUtil.isNil(this._toDateTime) + ) { + return true; + } + + if (!CollectionsUtil.isString(execution.startTime)) { + return false; + } + + const startTime = new Date(execution.startTime).getTime(); + + return ( + this._fromDateTime.getTime() <= startTime && + startTime <= this._toDateTime.getTime() + ); + }); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts index 988b1da474..5e1f2d90fe 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './executions-time-period.criteria'; +export * from "./executions-time-period.criteria"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html index dc538ab13e..6400066f93 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html @@ -4,9 +4,9 @@ --> -
-
- - -
-
- -
+
+
+ +
+
- -
-
- - -
-
- - -
-
-
-
- - -
-
-
+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
-

No executions found.

+

No executions found.

- - + + -
- -
+
+ +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.scss index 69c41ea9ef..e8b9d87f96 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.scss @@ -4,28 +4,28 @@ */ :host { - position: relative; - display: block; - width: 100%; - height: 100%; + position: relative; + display: block; + width: 100%; + height: 100%; - .time-filter__container { - display: flex; - align-items: center; - flex: 1; - justify-content: flex-end; - height: 2.5rem; - } + .time-filter__container { + display: flex; + align-items: center; + flex: 1; + justify-content: flex-end; + height: 2.5rem; + } - .execution-statuses-chart { - padding-right: 0 !important; - } + .execution-statuses-chart { + padding-right: 0 !important; + } - .job-executions__spinner { - left: 50%; - position: absolute; - top: 50%; - z-index: 1000; - transform: translate(-50%, -50%); - } + .job-executions__spinner { + left: 50%; + position: absolute; + top: 50%; + z-index: 1000; + transform: translate(-50%, -50%); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts index afbb03d29a..60bb834af8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts @@ -3,425 +3,491 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { DatePipe, Location } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { DatePipe, Location } from "@angular/common"; +import { ActivatedRoute, Router } from "@angular/router"; -import { distinctUntilChanged, map } from 'rxjs/operators'; +import { distinctUntilChanged, map } from "rxjs/operators"; -import { ClrDatagridSortOrder } from '@clr/angular'; +import { ClrDatagridSortOrder } from "@clr/angular"; import { - ASC, - CollectionsUtil, - ComponentModel, - ComponentService, - Criteria, - ErrorHandlerService, - ErrorRecord, - NavigationService, - OnTaurusModelChange, - OnTaurusModelError, - OnTaurusModelInit, - OnTaurusModelLoad, - RouterService, - RouteState, - TaurusBaseComponent, - URLStateManager -} from '@versatiledatakit/shared'; - -import { DataJobUtil, ErrorUtil } from '../../../../../shared/utils'; - -import { FiltersSortManager } from '../../../../../commons'; + ASC, + CollectionsUtil, + ComponentModel, + ComponentService, + Criteria, + ErrorHandlerService, + ErrorRecord, + NavigationService, + OnTaurusModelChange, + OnTaurusModelError, + OnTaurusModelInit, + OnTaurusModelLoad, + RouterService, + RouteState, + TaurusBaseComponent, + URLStateManager, +} from "@versatiledatakit/shared"; + +import { DataJobUtil, ErrorUtil } from "../../../../../shared/utils"; + +import { FiltersSortManager } from "../../../../../commons"; import { - DataJobExecutionOrder, - DataJobExecutions, - DataPipelinesRouteData, - JOB_EXECUTIONS_DATA_KEY, - JOB_NAME_REQ_PARAM, - ORDER_REQ_PARAM, - TEAM_NAME_REQ_PARAM -} from '../../../../../model'; + DataJobExecutionOrder, + DataJobExecutions, + DataPipelinesRouteData, + JOB_EXECUTIONS_DATA_KEY, + JOB_NAME_REQ_PARAM, + ORDER_REQ_PARAM, + TEAM_NAME_REQ_PARAM, +} from "../../../../../model"; -import { TASK_LOAD_JOB_EXECUTIONS } from '../../../../../state/tasks'; +import { TASK_LOAD_JOB_EXECUTIONS } from "../../../../../state/tasks"; -import { LOAD_JOB_ERROR_CODES } from '../../../../../state/error-codes'; +import { LOAD_JOB_ERROR_CODES } from "../../../../../state/error-codes"; -import { DataJobsService } from '../../../../../services'; +import { DataJobsService } from "../../../../../services"; -import { DataJobExecutionToGridDataJobExecution, GridDataJobExecution } from '../model/data-job-execution'; +import { + DataJobExecutionToGridDataJobExecution, + GridDataJobExecution, +} from "../model/data-job-execution"; import { - ExecutionsFilterCriteria, - ExecutionsFilterSortObject, - ExecutionsSortCriteria, - SORT_START_TIME_KEY, - SUPPORTED_EXECUTIONS_FILTER_CRITERIA, - SUPPORTED_EXECUTIONS_SORT_CRITERIA -} from '../model/executions-filters.model'; + ExecutionsFilterCriteria, + ExecutionsFilterSortObject, + ExecutionsSortCriteria, + SORT_START_TIME_KEY, + SUPPORTED_EXECUTIONS_FILTER_CRITERIA, + SUPPORTED_EXECUTIONS_SORT_CRITERIA, +} from "../model/executions-filters.model"; -import { GridCriteriaAndComparator } from '../data-job-executions-grid'; +import { GridCriteriaAndComparator } from "../data-job-executions-grid"; -import { ExecutionsTimePeriodCriteria } from './criteria/time-period'; +import { ExecutionsTimePeriodCriteria } from "./criteria/time-period"; interface SelectedDateTimePeriod { - from: Date; - to: Date; + from: Date; + to: Date; } @Component({ - selector: 'lib-data-job-executions-page', - templateUrl: './data-job-executions-page.component.html', - styleUrls: ['./data-job-executions-page.component.scss'], - providers: [DatePipe] + selector: "lib-data-job-executions-page", + templateUrl: "./data-job-executions-page.component.html", + styleUrls: ["./data-job-executions-page.component.scss"], + providers: [DatePipe], }) export class DataJobExecutionsPageComponent - extends TaurusBaseComponent - implements OnTaurusModelInit, OnTaurusModelLoad, OnTaurusModelChange, OnTaurusModelError, OnInit, OnDestroy + extends TaurusBaseComponent + implements + OnTaurusModelInit, + OnTaurusModelLoad, + OnTaurusModelChange, + OnTaurusModelError, + OnInit, + OnDestroy { - readonly uuid = 'DataJobExecutionsPageComponent'; - - teamName: string; - jobName: string; - isJobEditable = false; - - jobExecutions: GridDataJobExecution[] = []; - filteredJobExecutions: GridDataJobExecution[] = []; - minJobExecutionTime: Date; - loading = true; - initialLoading = true; - - /** - * ** Selected DateTime period in time period filter. - */ - selectedPeriod: SelectedDateTimePeriod = { - from: null, - to: null - }; - - /** - * ** Indicates whether time filter is chosen, period is selected. - */ - isPeriodSelected = false; - - /** - * ** Zoomed DateTime period, in duration chart. - */ - zoomedPeriod: SelectedDateTimePeriod = { - from: null, - to: null - }; - - /** - * ** Focused (highlighted) execution id in duration chart. - */ - highlightedExecutionId: string; - - /** - * ** Grid Criteria and Comparator from Executions Data Grid. - */ - gridCriteriaAndComparator: GridCriteriaAndComparator; - - /** - * ** Array of error code patterns that component should listen for in errors store. - */ - listenForErrorPatterns: string[] = [LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS].All]; - - /** - * ** Flag that indicates there is jobs executions load error. - */ - isComponentInErrorState = false; - - /** - * ** Executions filters sort manager for this page, that is injected to its children. - * - * - Singleton for the page instance including its children. - */ - readonly filtersSortManager: Readonly< - FiltersSortManager - >; - - /** - * ** Url state manager in context of this page. - * - * - Singleton for the page instance including its children. - */ - private readonly urlStateManager: URLStateManager; - - /** - * ** Constructor. - */ - constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - private readonly routerService: RouterService, - private readonly dataJobsService: DataJobsService, - private readonly errorHandlerService: ErrorHandlerService, - private readonly changeDetectorRef: ChangeDetectorRef, - private readonly router: Router, - private readonly location: Location, - private readonly datePipe: DatePipe + readonly uuid = "DataJobExecutionsPageComponent"; + + teamName: string; + jobName: string; + isJobEditable = false; + + jobExecutions: GridDataJobExecution[] = []; + filteredJobExecutions: GridDataJobExecution[] = []; + minJobExecutionTime: Date; + loading = true; + initialLoading = true; + + /** + * ** Selected DateTime period in time period filter. + */ + selectedPeriod: SelectedDateTimePeriod = { + from: null, + to: null, + }; + + /** + * ** Indicates whether time filter is chosen, period is selected. + */ + isPeriodSelected = false; + + /** + * ** Zoomed DateTime period, in duration chart. + */ + zoomedPeriod: SelectedDateTimePeriod = { + from: null, + to: null, + }; + + /** + * ** Focused (highlighted) execution id in duration chart. + */ + highlightedExecutionId: string; + + /** + * ** Grid Criteria and Comparator from Executions Data Grid. + */ + gridCriteriaAndComparator: GridCriteriaAndComparator; + + /** + * ** Array of error code patterns that component should listen for in errors store. + */ + listenForErrorPatterns: string[] = [ + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS].All, + ]; + + /** + * ** Flag that indicates there is jobs executions load error. + */ + isComponentInErrorState = false; + + /** + * ** Executions filters sort manager for this page, that is injected to its children. + * + * - Singleton for the page instance including its children. + */ + readonly filtersSortManager: Readonly< + FiltersSortManager< + ExecutionsFilterCriteria, + string, + ExecutionsSortCriteria, + ClrDatagridSortOrder + > + >; + + /** + * ** Url state manager in context of this page. + * + * - Singleton for the page instance including its children. + */ + private readonly urlStateManager: URLStateManager; + + /** + * ** Constructor. + */ + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + private readonly routerService: RouterService, + private readonly dataJobsService: DataJobsService, + private readonly errorHandlerService: ErrorHandlerService, + private readonly changeDetectorRef: ChangeDetectorRef, + private readonly router: Router, + private readonly location: Location, + private readonly datePipe: DatePipe, + ) { + super(componentService, navigationService, activatedRoute); + + this.urlStateManager = new URLStateManager( + router.url.split("?")[0], + location, + ); + this.filtersSortManager = new FiltersSortManager( + this.urlStateManager, + SUPPORTED_EXECUTIONS_FILTER_CRITERIA, + SUPPORTED_EXECUTIONS_SORT_CRITERIA, + ); + } + + doNavigateBack(): void { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.navigateBack({ "$.team": this.teamName }).then(); + } + + timeFilterChange(selectedPeriod: SelectedDateTimePeriod): void { + if ( + this.selectedPeriod.from === selectedPeriod.from && + this.selectedPeriod.to === selectedPeriod.to ) { - super(componentService, navigationService, activatedRoute); - - this.urlStateManager = new URLStateManager(router.url.split('?')[0], location); - this.filtersSortManager = new FiltersSortManager( - this.urlStateManager, - SUPPORTED_EXECUTIONS_FILTER_CRITERIA, - SUPPORTED_EXECUTIONS_SORT_CRITERIA - ); + return; } - doNavigateBack(): void { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.navigateBack({ '$.team': this.teamName }).then(); - } - - timeFilterChange(selectedPeriod: SelectedDateTimePeriod): void { - if (this.selectedPeriod.from === selectedPeriod.from && this.selectedPeriod.to === selectedPeriod.to) { - return; - } + this.selectedPeriod = selectedPeriod; - this.selectedPeriod = selectedPeriod; + this.isPeriodSelected = + this.selectedPeriod.from !== null && this.selectedPeriod.to !== null; - this.isPeriodSelected = this.selectedPeriod.from !== null && this.selectedPeriod.to !== null; + this._filterExecutions(); + } - this._filterExecutions(); - } + /** + * ** Executed whenever focus on execution id in duration chart changes. + */ + durationChartExecutionIdFocusChange(executionId: string): void { + this.highlightedExecutionId = executionId; + } - /** - * ** Executed whenever focus on execution id in duration chart changes. - */ - durationChartExecutionIdFocusChange(executionId: string): void { - this.highlightedExecutionId = executionId; - } - - durationChartZoomPeriodChange(zoomedPeriod: SelectedDateTimePeriod): void { - if ( - this.selectedPeriod.from === zoomedPeriod.from && - this.selectedPeriod.to === zoomedPeriod.to && - this.zoomedPeriod.from === zoomedPeriod.from && - this.zoomedPeriod.to === zoomedPeriod.to - ) { - return; - } - - this.zoomedPeriod = zoomedPeriod; - } - - gridCriteriaAndComparatorChange($event: GridCriteriaAndComparator): void { - this.gridCriteriaAndComparator = $event; - - this._filterExecutions(); - } - - refresh(): void { - this.fetchDataJobExecutions(); - } - - fetchDataJobExecutions(): void { - this.loading = true; - - this.dataJobsService.loadJobExecutions( - this.model - .withRequestParam(TEAM_NAME_REQ_PARAM, this.teamName) - .withRequestParam(JOB_NAME_REQ_PARAM, this.jobName) - .withRequestParam(ORDER_REQ_PARAM, { - property: 'startTime', - direction: ASC - } as DataJobExecutionOrder) - ); - - this.changeDetectorRef.markForCheck(); + durationChartZoomPeriodChange(zoomedPeriod: SelectedDateTimePeriod): void { + if ( + this.selectedPeriod.from === zoomedPeriod.from && + this.selectedPeriod.to === zoomedPeriod.to && + this.zoomedPeriod.from === zoomedPeriod.from && + this.zoomedPeriod.to === zoomedPeriod.to + ) { + return; } - /** - * @inheritDoc - */ - onModelInit(): void { - let isInitialized = false; - - this.subscriptions.push( - this.routerService - .getState() - .pipe( - distinctUntilChanged((a, b) => { - return CollectionsUtil.isEqual(a.queryParams, b.queryParams); - }) - ) - .subscribe((state) => { - if (!isInitialized) { - isInitialized = true; - - this._initialize(state); - } else { - // pass query params for popped state and let manager extract known filters and sort - // action is needed for Browser backward/forward actions that trigger router navigation - // tested only for "locationToUrl" update strategy for URLStateManager - this.filtersSortManager.bulkUpdate(state.queryParams as ExecutionsFilterSortObject, true); - } - }) + this.zoomedPeriod = zoomedPeriod; + } + + gridCriteriaAndComparatorChange($event: GridCriteriaAndComparator): void { + this.gridCriteriaAndComparator = $event; + + this._filterExecutions(); + } + + refresh(): void { + this.fetchDataJobExecutions(); + } + + fetchDataJobExecutions(): void { + this.loading = true; + + this.dataJobsService.loadJobExecutions( + this.model + .withRequestParam(TEAM_NAME_REQ_PARAM, this.teamName) + .withRequestParam(JOB_NAME_REQ_PARAM, this.jobName) + .withRequestParam(ORDER_REQ_PARAM, { + property: "startTime", + direction: ASC, + } as DataJobExecutionOrder), + ); + + this.changeDetectorRef.markForCheck(); + } + + /** + * @inheritDoc + */ + onModelInit(): void { + let isInitialized = false; + + this.subscriptions.push( + this.routerService + .getState() + .pipe( + distinctUntilChanged((a, b) => { + return CollectionsUtil.isEqual(a.queryParams, b.queryParams); + }), + ) + .subscribe((state) => { + if (!isInitialized) { + isInitialized = true; + + this._initialize(state); + } else { + // pass query params for popped state and let manager extract known filters and sort + // action is needed for Browser backward/forward actions that trigger router navigation + // tested only for "locationToUrl" update strategy for URLStateManager + this.filtersSortManager.bulkUpdate( + state.queryParams as ExecutionsFilterSortObject, + true, + ); + } + }), + ); + } + + /** + * @inheritDoc + */ + onModelLoad(): void { + this.loading = false; + this.initialLoading = false; + } + + /** + * @inheritDoc + */ + onModelChange(model: ComponentModel, task: string): void { + if (task === TASK_LOAD_JOB_EXECUTIONS) { + const executions: DataJobExecutions = model + .getComponentState() + .data.get(JOB_EXECUTIONS_DATA_KEY); + if (executions) { + this.dataJobsService.notifyForJobExecutions([...executions]); + + // eslint-disable-next-line @typescript-eslint/unbound-method + const runningExecution = executions.find( + DataJobUtil.isJobRunningPredicate, ); - } - - /** - * @inheritDoc - */ - onModelLoad(): void { - this.loading = false; - this.initialLoading = false; - } - - /** - * @inheritDoc - */ - onModelChange(model: ComponentModel, task: string): void { - if (task === TASK_LOAD_JOB_EXECUTIONS) { - const executions: DataJobExecutions = model.getComponentState().data.get(JOB_EXECUTIONS_DATA_KEY); - if (executions) { - this.dataJobsService.notifyForJobExecutions([...executions]); - - // eslint-disable-next-line @typescript-eslint/unbound-method - const runningExecution = executions.find(DataJobUtil.isJobRunningPredicate); - if (runningExecution) { - this.dataJobsService.notifyForRunningJobExecutionId(runningExecution.id); - } - } + if (runningExecution) { + this.dataJobsService.notifyForRunningJobExecutionId( + runningExecution.id, + ); } + } } - - /** - * @inheritDoc - */ - onModelError(model: ComponentModel, _task: string, newErrorRecords: ErrorRecord[]): void { - newErrorRecords.forEach((errorRecord) => { - const error = ErrorUtil.extractError(errorRecord.error); - - this.errorHandlerService.processError(error); - }); - } - - /** - * @inheritDoc - */ - override ngOnInit(): void { - // attach listener to ErrorStore and listen for Errors change - this.errors.onChange((store) => { - // if there is record for listened error code patterns set component in error state - this.isComponentInErrorState = store.hasCodePattern(...this.listenForErrorPatterns); - }); - - super.ngOnInit(); - } - - /** - * @inheritDoc - */ - override ngOnDestroy(): void { - this.filtersSortManager.cancelScheduledBrowserUrlUpdate(); - - super.ngOnDestroy(); - } - - private _initialize(state: RouteState): void { - const teamParamKey = state.getData('teamParamKey'); - this.teamName = state.getParam(teamParamKey); - - const jobParamKey = state.getData('jobParamKey'); - this.jobName = state.getParam(jobParamKey); - - this.isJobEditable = !!state.getData('editable'); - - // filters/sort manager have to be initialized before sending HTTP request to load executions - this._initializeFiltersSortManager(state); - - this._subscribeForExecutions(); - - this.fetchDataJobExecutions(); + } + + /** + * @inheritDoc + */ + onModelError( + model: ComponentModel, + _task: string, + newErrorRecords: ErrorRecord[], + ): void { + newErrorRecords.forEach((errorRecord) => { + const error = ErrorUtil.extractError(errorRecord.error); + + this.errorHandlerService.processError(error); + }); + } + + /** + * @inheritDoc + */ + override ngOnInit(): void { + // attach listener to ErrorStore and listen for Errors change + this.errors.onChange((store) => { + // if there is record for listened error code patterns set component in error state + this.isComponentInErrorState = store.hasCodePattern( + ...this.listenForErrorPatterns, + ); + }); + + super.ngOnInit(); + } + + /** + * @inheritDoc + */ + override ngOnDestroy(): void { + this.filtersSortManager.cancelScheduledBrowserUrlUpdate(); + + super.ngOnDestroy(); + } + + private _initialize(state: RouteState): void { + const teamParamKey = + state.getData("teamParamKey"); + this.teamName = state.getParam(teamParamKey); + + const jobParamKey = + state.getData("jobParamKey"); + this.jobName = state.getParam(jobParamKey); + + this.isJobEditable = + !!state.getData("editable"); + + // filters/sort manager have to be initialized before sending HTTP request to load executions + this._initializeFiltersSortManager(state); + + this._subscribeForExecutions(); + + this.fetchDataJobExecutions(); + } + + private _initializeFiltersSortManager(state: RouteState): void { + // update manager configuration + this.filtersSortManager.changeBaseUrl(state.absoluteRoutePath); + this.filtersSortManager.changeUpdateStrategy("locationToURL"); + + // update stored filters and sort criteria from Browser URL query params + this.filtersSortManager.bulkUpdate( + state.queryParams as ExecutionsFilterSortObject, + ); + + // if there is no sort applied through Browser URL, apply default sorting by Start Time Descending + if (!this.filtersSortManager.hasAnySort()) { + this.filtersSortManager.setSort( + SORT_START_TIME_KEY, + ClrDatagridSortOrder.DESC, + ); } - private _initializeFiltersSortManager(state: RouteState): void { - // update manager configuration - this.filtersSortManager.changeBaseUrl(state.absoluteRoutePath); - this.filtersSortManager.changeUpdateStrategy('locationToURL'); - - // update stored filters and sort criteria from Browser URL query params - this.filtersSortManager.bulkUpdate(state.queryParams as ExecutionsFilterSortObject); - - // if there is no sort applied through Browser URL, apply default sorting by Start Time Descending - if (!this.filtersSortManager.hasAnySort()) { - this.filtersSortManager.setSort(SORT_START_TIME_KEY, ClrDatagridSortOrder.DESC); - } - - // update Browser URL with replace state, normalized to only manager known criteria - this.filtersSortManager.updateBrowserUrl('replaceToURL', true); + // update Browser URL with replace state, normalized to only manager known criteria + this.filtersSortManager.updateBrowserUrl("replaceToURL", true); + } + + private _subscribeForExecutions(): void { + this.subscriptions.push( + this.dataJobsService + .getNotifiedForJobExecutions() + .pipe( + map( + DataJobExecutionToGridDataJobExecution.convertToDataJobExecution( + this.datePipe, + ), + ), + ) + .subscribe({ + next: (values) => { + this.jobExecutions = values; + + this._filterExecutions(); + + if (this.jobExecutions.length > 0) { + const oldestExecutionStartTime = [...this.jobExecutions] + .sort((ex1, ex2) => (ex1.startTime < ex2.startTime ? 1 : -1)) + .pop().startTime; + const newMinJobExecutionsTime = new Date( + oldestExecutionStartTime, + ); + + if ( + CollectionsUtil.isNil(this.minJobExecutionTime) || + newMinJobExecutionsTime.getTime() - + this.minJobExecutionTime.getTime() !== + 0 + ) { + this.minJobExecutionTime = newMinJobExecutionsTime; + } + } else { + this.minJobExecutionTime = null; + } + }, + error: (error: unknown) => { + console.error(error); + }, + }), + ); + } + + private _filterExecutions(): void { + let timePeriodCriteria: Criteria; + let executionsFilteredAndSorted: GridDataJobExecution[]; + + if ( + CollectionsUtil.isDefined(this.selectedPeriod.from) && + CollectionsUtil.isDefined(this.selectedPeriod.to) + ) { + timePeriodCriteria = new ExecutionsTimePeriodCriteria( + this.selectedPeriod.from, + this.selectedPeriod.to, + ); + // execute filter by time period + executionsFilteredAndSorted = timePeriodCriteria.meetCriteria( + this.jobExecutions, + ); + } else { + executionsFilteredAndSorted = [...this.jobExecutions]; } - private _subscribeForExecutions(): void { - this.subscriptions.push( - this.dataJobsService - .getNotifiedForJobExecutions() - .pipe(map(DataJobExecutionToGridDataJobExecution.convertToDataJobExecution(this.datePipe))) - .subscribe({ - next: (values) => { - this.jobExecutions = values; - - this._filterExecutions(); - - if (this.jobExecutions.length > 0) { - const oldestExecutionStartTime = [...this.jobExecutions] - .sort((ex1, ex2) => (ex1.startTime < ex2.startTime ? 1 : -1)) - .pop().startTime; - const newMinJobExecutionsTime = new Date(oldestExecutionStartTime); - - if ( - CollectionsUtil.isNil(this.minJobExecutionTime) || - newMinJobExecutionsTime.getTime() - this.minJobExecutionTime.getTime() !== 0 - ) { - this.minJobExecutionTime = newMinJobExecutionsTime; - } - } else { - this.minJobExecutionTime = null; - } - }, - error: (error: unknown) => { - console.error(error); - } - }) + if (this.gridCriteriaAndComparator) { + if (this.gridCriteriaAndComparator.filter) { + executionsFilteredAndSorted = + this.gridCriteriaAndComparator.filter.meetCriteria( + executionsFilteredAndSorted, + ); + } + + if (this.gridCriteriaAndComparator.sort) { + executionsFilteredAndSorted = executionsFilteredAndSorted.sort( + this.gridCriteriaAndComparator.sort.compare.bind( + this.gridCriteriaAndComparator.sort, + ) as (a: GridDataJobExecution, b: GridDataJobExecution) => number, ); + } } - private _filterExecutions(): void { - let timePeriodCriteria: Criteria; - let executionsFilteredAndSorted: GridDataJobExecution[]; - - if (CollectionsUtil.isDefined(this.selectedPeriod.from) && CollectionsUtil.isDefined(this.selectedPeriod.to)) { - timePeriodCriteria = new ExecutionsTimePeriodCriteria(this.selectedPeriod.from, this.selectedPeriod.to); - // execute filter by time period - executionsFilteredAndSorted = timePeriodCriteria.meetCriteria(this.jobExecutions); - } else { - executionsFilteredAndSorted = [...this.jobExecutions]; - } - - if (this.gridCriteriaAndComparator) { - if (this.gridCriteriaAndComparator.filter) { - executionsFilteredAndSorted = this.gridCriteriaAndComparator.filter.meetCriteria(executionsFilteredAndSorted); - } - - if (this.gridCriteriaAndComparator.sort) { - executionsFilteredAndSorted = executionsFilteredAndSorted.sort( - this.gridCriteriaAndComparator.sort.compare.bind(this.gridCriteriaAndComparator.sort) as ( - a: GridDataJobExecution, - b: GridDataJobExecution - ) => number - ); - } - } + this.filteredJobExecutions = executionsFilteredAndSorted; - this.filteredJobExecutions = executionsFilteredAndSorted; - - this.minJobExecutionTime = new Date(this.minJobExecutionTime); - } + this.minJobExecutionTime = new Date(this.minJobExecutionTime); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/index.ts index fd311d3bcb..88ec7d4b9d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-executions-page.component'; +export * from "./data-job-executions-page.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.html index 3b12f32fc4..e5390f8f58 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.html @@ -4,20 +4,20 @@ -->
-
- - - Click and drag to zoom -
+
+ + + Click and drag to zoom +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.scss index 5b8dbc8d7d..bd4ef47f43 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.scss @@ -4,19 +4,19 @@ */ div.duration-chart { - height: 200px; - position: relative; + height: 200px; + position: relative; - button.reset-zoom { - position: absolute; - top: -1rem; - left: 3rem; - } + button.reset-zoom { + position: absolute; + top: -1rem; + left: 3rem; + } - span.zoom-tooltip { - position: absolute; - top: -0.8rem; - opacity: 0.5; - left: 2.5rem; - } + span.zoom-tooltip { + position: absolute; + top: -0.8rem; + opacity: 0.5; + left: 2.5rem; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts index 79e14d7e51..b4433a8e06 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts @@ -3,434 +3,526 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; - -import { ActiveElement, Chart, ChartData, registerables, ScatterDataPoint, TimeUnit } from 'chart.js'; -import ChartDataLabels from 'chartjs-plugin-datalabels'; -import zoomPlugin from 'chartjs-plugin-zoom'; -import 'chartjs-adapter-date-fns'; - -import { CollectionsUtil } from '@versatiledatakit/shared'; - -import { DateUtil } from '../../../../../shared/utils'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus } from '../../../../../model'; - -import { DataJobExecutionToGridDataJobExecution, GridDataJobExecution } from '../model'; +import { DatePipe } from "@angular/common"; +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from "@angular/core"; + +import { + ActiveElement, + Chart, + ChartData, + registerables, + ScatterDataPoint, + TimeUnit, +} from "chart.js"; +import ChartDataLabels from "chartjs-plugin-datalabels"; +import zoomPlugin from "chartjs-plugin-zoom"; +import "chartjs-adapter-date-fns"; + +import { CollectionsUtil } from "@versatiledatakit/shared"; + +import { DateUtil } from "../../../../../shared/utils"; + +import { + DATA_PIPELINES_DATE_TIME_FORMAT, + DataJobExecutionStatus, +} from "../../../../../model"; + +import { + DataJobExecutionToGridDataJobExecution, + GridDataJobExecution, +} from "../model"; type CustomChartData = Partial & { - startTime: number; - duration: number; - endTime: string; - status: DataJobExecutionStatus; - opId: string; - id: string; + startTime: number; + duration: number; + endTime: string; + status: DataJobExecutionStatus; + opId: string; + id: string; }; interface ZoomPeriod { - from: Date; - to: Date; + from: Date; + to: Date; } @Component({ - selector: 'lib-execution-duration-chart', - templateUrl: './execution-duration-chart.component.html', - styleUrls: ['./execution-duration-chart.component.scss'], - providers: [DatePipe] + selector: "lib-execution-duration-chart", + templateUrl: "./execution-duration-chart.component.html", + styleUrls: ["./execution-duration-chart.component.scss"], + providers: [DatePipe], }) export class ExecutionDurationChartComponent implements OnInit, OnChanges { - @Input() jobExecutions: GridDataJobExecution[] = []; - - /** - * ** Flag that indicates if duration chart is zoomed or not. - */ - @Input() chartZoomed = false; - - /** - * ** Emits event whenever focus on execution changes. - * - * - Value could be either executionId or null. - */ - @Output() executionIdFocused = new EventEmitter(); - - /** - * ** Event Emitter that emits events on every user zoom period change in duration chart or reset zoom. - */ - @Output() zoomPeriodChanged: EventEmitter = new EventEmitter(); - - /** - * ** Reference to Duration chart instance. - */ - chart: Chart<'line', CustomChartData[], number>; - - /** - * ** Currently focussed execution id, it could be either string if there is focussed execution or null if nothing is focussed. - * @private - */ - private _focusedExecutionId: CustomChartData['id']; - - /** - * ** Zoom selection reference with from and to values. - * @private - */ - private _zoomPeriod: ZoomPeriod = { - from: null, - to: null + @Input() jobExecutions: GridDataJobExecution[] = []; + + /** + * ** Flag that indicates if duration chart is zoomed or not. + */ + @Input() chartZoomed = false; + + /** + * ** Emits event whenever focus on execution changes. + * + * - Value could be either executionId or null. + */ + @Output() executionIdFocused = new EventEmitter(); + + /** + * ** Event Emitter that emits events on every user zoom period change in duration chart or reset zoom. + */ + @Output() zoomPeriodChanged: EventEmitter = + new EventEmitter(); + + /** + * ** Reference to Duration chart instance. + */ + chart: Chart<"line", CustomChartData[], number>; + + /** + * ** Currently focussed execution id, it could be either string if there is focussed execution or null if nothing is focussed. + * @private + */ + private _focusedExecutionId: CustomChartData["id"]; + + /** + * ** Zoom selection reference with from and to values. + * @private + */ + private _zoomPeriod: ZoomPeriod = { + from: null, + to: null, + }; + + constructor(private readonly datePipe: DatePipe) { + Chart.register(...registerables, ChartDataLabels, zoomPlugin); + } + + resetZoom() { + this._zoomPeriod = { + from: null, + to: null, }; - constructor(private readonly datePipe: DatePipe) { - Chart.register(...registerables, ChartDataLabels, zoomPlugin); - } - - resetZoom() { - this._zoomPeriod = { - from: null, - to: null - }; + this.zoomPeriodChanged.next(this._zoomPeriod); + } - this.zoomPeriodChanged.next(this._zoomPeriod); - } - - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges): void { - if (!changes['jobExecutions'].firstChange) { - this._updateChart(); - } + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + if (!changes["jobExecutions"].firstChange) { + this._updateChart(); } + } + + /** + * @inheritDoc + */ + ngOnInit(): void { + this._initChart(); + } + + private _initChart(): void { + const chartData: CustomChartData[] = this._getChartData(); + const unit: TimeUnit = this._getTimeScaleUnit(chartData); + const [min, max] = this._getMinMaxExecutionTupleAdjusted(chartData, unit); + + const data: ChartData<"line", CustomChartData[], number> = { + labels: this._getChartLabels(), + datasets: [ + { + data: chartData, + fill: false, + pointRadius: 3, + pointBorderColor: (context) => + DataJobExecutionToGridDataJobExecution.resolveColor( + (context.raw as { status: string })?.status, + ), + pointBackgroundColor: (context) => + DataJobExecutionToGridDataJobExecution.resolveColor( + (context.raw as { status: string })?.status, + ), + pointBorderWidth: 3, + parsing: { + xAxisKey: "startTime", + yAxisKey: "duration", + }, + }, + ], + }; - /** - * @inheritDoc - */ - ngOnInit(): void { - this._initChart(); - } + this.chart = new Chart<"line", CustomChartData[], number>("durationChart", { + type: "line", + data, + options: { + // callback listen for hover events in duration chart and process events + onHover: (event, activeElements) => { + this._emitFocussedExecutionId(activeElements); + }, + showLine: false, + scales: { + x: { + type: "time", + time: { + unit, + }, + min, + max, + }, + y: { + title: { + display: true, + text: `Duration ${this._getDurationUnit().name}`, + }, + }, + }, + maintainAspectRatio: false, + plugins: { + zoom: { + zoom: { + drag: { + enabled: true, + }, + mode: "x", + onZoomComplete: (context) => { + const from = new Date( + Math.floor(context.chart.scales["x"].min), + ); + const to = new Date(Math.ceil(context.chart.scales["x"].max)); - private _initChart(): void { - const chartData: CustomChartData[] = this._getChartData(); - const unit: TimeUnit = this._getTimeScaleUnit(chartData); - const [min, max] = this._getMinMaxExecutionTupleAdjusted(chartData, unit); - - const data: ChartData<'line', CustomChartData[], number> = { - labels: this._getChartLabels(), - datasets: [ - { - data: chartData, - fill: false, - pointRadius: 3, - pointBorderColor: (context) => - DataJobExecutionToGridDataJobExecution.resolveColor((context.raw as { status: string })?.status), - pointBackgroundColor: (context) => - DataJobExecutionToGridDataJobExecution.resolveColor((context.raw as { status: string })?.status), - pointBorderWidth: 3, - parsing: { - xAxisKey: 'startTime', - yAxisKey: 'duration' - } + if ( + this._zoomPeriod.from === from && + this._zoomPeriod.to === to + ) { + return; } - ] - }; - - this.chart = new Chart<'line', CustomChartData[], number>('durationChart', { - type: 'line', - data, - options: { - // callback listen for hover events in duration chart and process events - onHover: (event, activeElements) => { - this._emitFocussedExecutionId(activeElements); - }, - showLine: false, - scales: { - x: { - type: 'time', - time: { - unit - }, - min, - max - }, - y: { - title: { - display: true, - text: `Duration ${this._getDurationUnit().name}` - } - } - }, - maintainAspectRatio: false, - plugins: { - zoom: { - zoom: { - drag: { - enabled: true - }, - mode: 'x', - onZoomComplete: (context) => { - const from = new Date(Math.floor(context.chart.scales['x'].min)); - const to = new Date(Math.ceil(context.chart.scales['x'].max)); - - if (this._zoomPeriod.from === from && this._zoomPeriod.to === to) { - return; - } - - this._zoomPeriod = { - from, - to - }; - - this.zoomPeriodChanged.next(this._zoomPeriod); - } - } - }, - datalabels: { - display: false - }, - legend: { - display: false - }, - tooltip: { - callbacks: { - label: (context) => { - const rawValues = context.raw as CustomChartData; - - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - return ( - `Duration: ${context.parsed.y} | ${rawValues.status}` + - (rawValues.endTime - ? ` | End: ${this.datePipe.transform(rawValues.endTime, DATA_PIPELINES_DATE_TIME_FORMAT, 'UTC')}` - : '') - ); - } - } - } - } - } - }); - } - private _updateChart(): void { - const chartLabels: number[] = this._getChartLabels(); - const chartData: CustomChartData[] = this._getChartData(); - const unit: TimeUnit = this._getTimeScaleUnit(chartData); - const [min, max] = this._getMinMaxExecutionTupleAdjusted(chartData, unit); + this._zoomPeriod = { + from, + to, + }; - this.chart.data.labels = chartLabels; - this.chart.data.datasets[0].data = chartData; - - this.chart.options.scales['x'] = { - type: 'time', - time: { - unit + this.zoomPeriodChanged.next(this._zoomPeriod); + }, }, - min, - max - }; + }, + datalabels: { + display: false, + }, + legend: { + display: false, + }, + tooltip: { + callbacks: { + label: (context) => { + const rawValues = context.raw as CustomChartData; + + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + return ( + `Duration: ${context.parsed.y} | ${rawValues.status}` + + (rawValues.endTime + ? ` | End: ${this.datePipe.transform(rawValues.endTime, DATA_PIPELINES_DATE_TIME_FORMAT, "UTC")}` + : "") + ); + }, + }, + }, + }, + }, + }); + } + + private _updateChart(): void { + const chartLabels: number[] = this._getChartLabels(); + const chartData: CustomChartData[] = this._getChartData(); + const unit: TimeUnit = this._getTimeScaleUnit(chartData); + const [min, max] = this._getMinMaxExecutionTupleAdjusted(chartData, unit); + + this.chart.data.labels = chartLabels; + this.chart.data.datasets[0].data = chartData; + + this.chart.options.scales["x"] = { + type: "time", + time: { + unit, + }, + min, + max, + }; - this.chart.update(); + this.chart.update(); + } + + private _getChartLabels(): number[] { + return this.jobExecutions.map((execution) => + DateUtil.normalizeToUTC(execution.startTime).getTime(), + ); + } + + private _getChartData(): CustomChartData[] { + const divider = this._getDurationUnit().divider; + + return this.jobExecutions + .map((execution) => { + return { + startTime: DateUtil.normalizeToUTC(execution.startTime).getTime(), + duration: + Math.round( + (this._getJobDurationSeconds(execution) / divider) * 100, + ) / 100, + endTime: execution.endTime ? execution.endTime : undefined, + status: execution.status, + opId: execution.opId, + id: execution.id, + } as CustomChartData; + }) + .sort((ex1, ex2) => ex1.startTime - ex2.startTime); + } + + private _getTimeScaleUnit(chartData: CustomChartData[]): TimeUnit { + const [min, max] = this._getMinMaxExecutionTuple(chartData); + + if (CollectionsUtil.isNil(min) || CollectionsUtil.isNil(max)) { + return "day"; } - private _getChartLabels(): number[] { - return this.jobExecutions.map((execution) => DateUtil.normalizeToUTC(execution.startTime).getTime()); - } + const _min = CollectionsUtil.isNumber(min) ? min : new Date(min).getTime(); + const _max = CollectionsUtil.isNumber(max) ? max : new Date(max).getTime(); + const diff = _max - _min; - private _getChartData(): CustomChartData[] { - const divider = this._getDurationUnit().divider; - - return this.jobExecutions - .map((execution) => { - return { - startTime: DateUtil.normalizeToUTC(execution.startTime).getTime(), - duration: Math.round((this._getJobDurationSeconds(execution) / divider) * 100) / 100, - endTime: execution.endTime ? execution.endTime : undefined, - status: execution.status, - opId: execution.opId, - id: execution.id - } as CustomChartData; - }) - .sort((ex1, ex2) => ex1.startTime - ex2.startTime); + if ( + diff > + this._getTimeUnitMilliseconds("year") + + this._getTimeUnitMilliseconds("second") + ) { + return "year"; } - private _getTimeScaleUnit(chartData: CustomChartData[]): TimeUnit { - const [min, max] = this._getMinMaxExecutionTuple(chartData); - - if (CollectionsUtil.isNil(min) || CollectionsUtil.isNil(max)) { - return 'day'; - } - - const _min = CollectionsUtil.isNumber(min) ? min : new Date(min).getTime(); - const _max = CollectionsUtil.isNumber(max) ? max : new Date(max).getTime(); - const diff = _max - _min; - - if (diff > this._getTimeUnitMilliseconds('year') + this._getTimeUnitMilliseconds('second')) { - return 'year'; - } - - if (diff > this._getTimeUnitMilliseconds('month') + this._getTimeUnitMilliseconds('second')) { - return 'month'; - } - - if (diff > 2 * this._getTimeUnitMilliseconds('week')) { - return 'week'; - } - - if (diff > this._getTimeUnitMilliseconds('day') + this._getTimeUnitMilliseconds('second')) { - return 'day'; - } - - if (diff > this._getTimeUnitMilliseconds('hour') + this._getTimeUnitMilliseconds('second')) { - return 'hour'; - } - - if (diff > this._getTimeUnitMilliseconds('minute') + this._getTimeUnitMilliseconds('millisecond')) { - return 'minute'; - } - - if (diff > this._getTimeUnitMilliseconds('second') + this._getTimeUnitMilliseconds('millisecond')) { - return 'second'; - } - - return 'millisecond'; + if ( + diff > + this._getTimeUnitMilliseconds("month") + + this._getTimeUnitMilliseconds("second") + ) { + return "month"; } - private _getDurationUnit(): { name: string; divider: number } { - const maxDurationSeconds = this._getMaxDurationSeconds(); - - if (maxDurationSeconds > 60) { - return maxDurationSeconds > 3600 ? { name: 'hours', divider: 3600 } : { name: 'minutes', divider: 60 }; - } else { - return { name: 'seconds', divider: 1 }; - } + if (diff > 2 * this._getTimeUnitMilliseconds("week")) { + return "week"; } - private _getMaxDurationSeconds(): number { - return this.jobExecutions - .map((execution) => this._getJobDurationSeconds(execution)) - .sort((v1, v2) => v1 - v2) - .pop(); + if ( + diff > + this._getTimeUnitMilliseconds("day") + + this._getTimeUnitMilliseconds("second") + ) { + return "day"; } - private _getJobDurationSeconds(execution: GridDataJobExecution): number { - const endTime = execution.endTime ? new Date(execution.endTime).getTime() : Date.now(); - const delta = endTime - new Date(execution.startTime).getTime(); - - return delta / 1000; + if ( + diff > + this._getTimeUnitMilliseconds("hour") + + this._getTimeUnitMilliseconds("second") + ) { + return "hour"; } - private _emitFocussedExecutionId(activeElements: ActiveElement[]): void { - if (activeElements.length > 0) { - const element: { $context?: { raw?: CustomChartData } } = activeElements[0].element as unknown; - const executionId = element?.$context?.raw?.id ?? null; - - // if event emits that element is focussed and that value is same as previous skip processing - if (this._focusedExecutionId === executionId) { - return; - } - - // when element is focused for the first time, save executionId in component context - this._focusedExecutionId = executionId; - // emit executionId to parent component - this.executionIdFocused.next(executionId); - } else { - // if event emits that no element is focussed and that value is same as previous skip processing - if (!this._focusedExecutionId) { - return; - } - - // when focused element lose focus clear executionId from component context - this._focusedExecutionId = null; - // emit null value to parent component - this.executionIdFocused.next(null); - } + if ( + diff > + this._getTimeUnitMilliseconds("minute") + + this._getTimeUnitMilliseconds("millisecond") + ) { + return "minute"; } - private _getMinMaxExecutionTuple(chartData: CustomChartData[]): [number, number] { - if (chartData.length === 0) { - if (CollectionsUtil.isDate(this._zoomPeriod.from) && CollectionsUtil.isDate(this._zoomPeriod.to)) { - return [this._zoomPeriod.from.getTime(), this._zoomPeriod.to.getTime()]; - } - - return [null, null]; - } + if ( + diff > + this._getTimeUnitMilliseconds("second") + + this._getTimeUnitMilliseconds("millisecond") + ) { + return "second"; + } - if (chartData.length === 1) { - if (CollectionsUtil.isDate(this._zoomPeriod.from) && CollectionsUtil.isDate(this._zoomPeriod.to)) { - if (this._zoomPeriod.to.getTime() - this._zoomPeriod.from.getTime() > 5 * this._getTimeUnitMilliseconds('minute')) { - return [this._zoomPeriod.from.getTime(), this._zoomPeriod.to.getTime()]; - } - } + return "millisecond"; + } - return [chartData[0].startTime, chartData[0].startTime]; - } + private _getDurationUnit(): { name: string; divider: number } { + const maxDurationSeconds = this._getMaxDurationSeconds(); - return [chartData[0].startTime, chartData[chartData.length - 1].startTime]; + if (maxDurationSeconds > 60) { + return maxDurationSeconds > 3600 + ? { name: "hours", divider: 3600 } + : { name: "minutes", divider: 60 }; + } else { + return { name: "seconds", divider: 1 }; + } + } + + private _getMaxDurationSeconds(): number { + return this.jobExecutions + .map((execution) => this._getJobDurationSeconds(execution)) + .sort((v1, v2) => v1 - v2) + .pop(); + } + + private _getJobDurationSeconds(execution: GridDataJobExecution): number { + const endTime = execution.endTime + ? new Date(execution.endTime).getTime() + : Date.now(); + const delta = endTime - new Date(execution.startTime).getTime(); + + return delta / 1000; + } + + private _emitFocussedExecutionId(activeElements: ActiveElement[]): void { + if (activeElements.length > 0) { + const element: { $context?: { raw?: CustomChartData } } = + activeElements[0].element as unknown; + const executionId = element?.$context?.raw?.id ?? null; + + // if event emits that element is focussed and that value is same as previous skip processing + if (this._focusedExecutionId === executionId) { + return; + } + + // when element is focused for the first time, save executionId in component context + this._focusedExecutionId = executionId; + // emit executionId to parent component + this.executionIdFocused.next(executionId); + } else { + // if event emits that no element is focussed and that value is same as previous skip processing + if (!this._focusedExecutionId) { + return; + } + + // when focused element lose focus clear executionId from component context + this._focusedExecutionId = null; + // emit null value to parent component + this.executionIdFocused.next(null); + } + } + + private _getMinMaxExecutionTuple( + chartData: CustomChartData[], + ): [number, number] { + if (chartData.length === 0) { + if ( + CollectionsUtil.isDate(this._zoomPeriod.from) && + CollectionsUtil.isDate(this._zoomPeriod.to) + ) { + return [this._zoomPeriod.from.getTime(), this._zoomPeriod.to.getTime()]; + } + + return [null, null]; } - private _getMinMaxExecutionTupleAdjusted(chartData: CustomChartData[], unit: TimeUnit): [number, number] { - const [min, max] = this._getMinMaxExecutionTuple(chartData); - - let adjustment: number; - - switch (unit) { - case 'millisecond': - adjustment = 10 * this._getTimeUnitMilliseconds('millisecond'); - break; - case 'second': - adjustment = 5 * this._getTimeUnitMilliseconds('second'); - break; - case 'minute': - adjustment = 5 * this._getTimeUnitMilliseconds('minute'); - break; - case 'hour': - adjustment = 2 * this._getTimeUnitMilliseconds('hour'); - break; - case 'day': - adjustment = 15 * this._getTimeUnitMilliseconds('hour'); - break; - case 'week': - adjustment = 3 * this._getTimeUnitMilliseconds('day'); - break; - case 'month': - adjustment = this._getTimeUnitMilliseconds('month'); - break; - case 'year': - adjustment = this._getTimeUnitMilliseconds('year'); - break; - default: - console.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Taurus DataPipelines ExecutionDurationChartComponent unsupported time format unit ${unit}` - ); + if (chartData.length === 1) { + if ( + CollectionsUtil.isDate(this._zoomPeriod.from) && + CollectionsUtil.isDate(this._zoomPeriod.to) + ) { + if ( + this._zoomPeriod.to.getTime() - this._zoomPeriod.from.getTime() > + 5 * this._getTimeUnitMilliseconds("minute") + ) { + return [ + this._zoomPeriod.from.getTime(), + this._zoomPeriod.to.getTime(), + ]; } + } - return [min - adjustment, max + adjustment]; + return [chartData[0].startTime, chartData[0].startTime]; } - private _getTimeUnitMilliseconds(unit: 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'): number { - switch (unit) { - case 'millisecond': - return 1; - case 'second': - return 1000; - case 'minute': - return 1000 * 60; - case 'hour': - return 1000 * 60 * 60; - case 'day': - return 1000 * 60 * 60 * 24; - case 'week': - return 1000 * 60 * 60 * 24 * 7; - case 'month': - return 1000 * 60 * 60 * 24 * 31; - case 'year': - return 1000 * 60 * 60 * 24 * 365; - default: - console.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Taurus DataPipelines ExecutionDurationChartComponent unsupported time format unit ${unit}` - ); + return [chartData[0].startTime, chartData[chartData.length - 1].startTime]; + } + + private _getMinMaxExecutionTupleAdjusted( + chartData: CustomChartData[], + unit: TimeUnit, + ): [number, number] { + const [min, max] = this._getMinMaxExecutionTuple(chartData); + + let adjustment: number; + + switch (unit) { + case "millisecond": + adjustment = 10 * this._getTimeUnitMilliseconds("millisecond"); + break; + case "second": + adjustment = 5 * this._getTimeUnitMilliseconds("second"); + break; + case "minute": + adjustment = 5 * this._getTimeUnitMilliseconds("minute"); + break; + case "hour": + adjustment = 2 * this._getTimeUnitMilliseconds("hour"); + break; + case "day": + adjustment = 15 * this._getTimeUnitMilliseconds("hour"); + break; + case "week": + adjustment = 3 * this._getTimeUnitMilliseconds("day"); + break; + case "month": + adjustment = this._getTimeUnitMilliseconds("month"); + break; + case "year": + adjustment = this._getTimeUnitMilliseconds("year"); + break; + default: + console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Taurus DataPipelines ExecutionDurationChartComponent unsupported time format unit ${unit}`, + ); + } - return 0; - } + return [min - adjustment, max + adjustment]; + } + + private _getTimeUnitMilliseconds( + unit: + | "millisecond" + | "second" + | "minute" + | "hour" + | "day" + | "week" + | "month" + | "year", + ): number { + switch (unit) { + case "millisecond": + return 1; + case "second": + return 1000; + case "minute": + return 1000 * 60; + case "hour": + return 1000 * 60 * 60; + case "day": + return 1000 * 60 * 60 * 24; + case "week": + return 1000 * 60 * 60 * 24 * 7; + case "month": + return 1000 * 60 * 60 * 24 * 31; + case "year": + return 1000 * 60 * 60 * 24 * 365; + default: + console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Taurus DataPipelines ExecutionDurationChartComponent unsupported time format unit ${unit}`, + ); + + return 0; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/index.ts index 4dea12c3a6..3678c389f5 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './execution-duration-chart.component'; +export * from "./execution-duration-chart.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.html index 591943e34e..f99614775f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.html @@ -4,14 +4,14 @@ -->
-
- -
- {{ totalExecutions }}
- Executions -
+
+ +
+ {{ totalExecutions }}
+ Executions
+
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.scss index 8eb1f2d24c..0d5f5d22d0 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.scss @@ -4,16 +4,16 @@ */ div.status-chart { - height: 200px; - position: relative; - margin-top: 15px; + height: 200px; + position: relative; + margin-top: 15px; - div.inner-text { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - font-size: 18px; - } + div.inner-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + font-size: 18px; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.ts index e299aedb4f..7bee068b79 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/execution-status-chart.component.ts @@ -3,106 +3,123 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; - -import { Chart, registerables } from 'chart.js'; -import ChartDataLabels from 'chartjs-plugin-datalabels'; - -import { DataJobExecutionToGridDataJobExecution, GridDataJobExecution } from '../model/data-job-execution'; +import { + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from "@angular/core"; + +import { Chart, registerables } from "chart.js"; +import ChartDataLabels from "chartjs-plugin-datalabels"; + +import { + DataJobExecutionToGridDataJobExecution, + GridDataJobExecution, +} from "../model/data-job-execution"; @Component({ - selector: 'lib-execution-status-chart', - templateUrl: './execution-status-chart.component.html', - styleUrls: ['./execution-status-chart.component.scss'] + selector: "lib-execution-status-chart", + templateUrl: "./execution-status-chart.component.html", + styleUrls: ["./execution-status-chart.component.scss"], }) export class ExecutionStatusChartComponent implements OnInit, OnChanges { - @Input() jobExecutions: GridDataJobExecution[]; - - totalExecutions: number; - chart: Chart; - - constructor() { - Chart.register(...registerables, ChartDataLabels); - } - - getDoughnutLabels(): string[] { - return this.jobExecutions.map((execution) => execution.status as string).filter((item, i, ar) => ar.indexOf(item) === i); - } - - getDoughnutData(): number[] { - const data: number[] = []; - - this.getDoughnutLabels().forEach((label) => - data.push(this.jobExecutions.filter((execution) => (execution.status as string) === label).length) - ); - - return data; - } - - getDoughnutLabelColors(): string[] { - const colors: string[] = []; - const statusColorMap = DataJobExecutionToGridDataJobExecution.getStatusColorsMap(); - - this.getDoughnutLabels().forEach((label) => { - colors.push(statusColorMap[label] as string); - }); - - return colors; - } - - ngOnChanges(changes: SimpleChanges): void { - if (!changes['jobExecutions'].isFirstChange()) { - this.totalExecutions = this.jobExecutions.length; - this.chart.data.labels = this.getDoughnutLabels(); - this.chart.data.datasets[0].backgroundColor = this.getDoughnutLabelColors(); - this.chart.data.datasets[0].data = this.getDoughnutData(); - this.chart.update(); - } - } - - ngOnInit(): void { - this.totalExecutions = this.jobExecutions.length; - - const data = { - labels: this.getDoughnutLabels(), - datasets: [ - { - data: this.getDoughnutData(), - backgroundColor: this.getDoughnutLabelColors(), - hoverOffset: 4 - } - ] - }; - - this.chart = new Chart('statusChart', { - type: 'doughnut', - data, - options: { - spacing: 1, - elements: { - arc: { - borderWidth: 0 - } - }, - cutout: 70, - maintainAspectRatio: false, - plugins: { - legend: { - display: false, - position: 'left' - }, - datalabels: { - color: 'black', - font: { - size: 16 - } - }, - tooltip: { - xAlign: 'center', - yAlign: 'center' - } - } - } - }); + @Input() jobExecutions: GridDataJobExecution[]; + + totalExecutions: number; + chart: Chart; + + constructor() { + Chart.register(...registerables, ChartDataLabels); + } + + getDoughnutLabels(): string[] { + return this.jobExecutions + .map((execution) => execution.status as string) + .filter((item, i, ar) => ar.indexOf(item) === i); + } + + getDoughnutData(): number[] { + const data: number[] = []; + + this.getDoughnutLabels().forEach((label) => + data.push( + this.jobExecutions.filter( + (execution) => (execution.status as string) === label, + ).length, + ), + ); + + return data; + } + + getDoughnutLabelColors(): string[] { + const colors: string[] = []; + const statusColorMap = + DataJobExecutionToGridDataJobExecution.getStatusColorsMap(); + + this.getDoughnutLabels().forEach((label) => { + colors.push(statusColorMap[label] as string); + }); + + return colors; + } + + ngOnChanges(changes: SimpleChanges): void { + if (!changes["jobExecutions"].isFirstChange()) { + this.totalExecutions = this.jobExecutions.length; + this.chart.data.labels = this.getDoughnutLabels(); + this.chart.data.datasets[0].backgroundColor = + this.getDoughnutLabelColors(); + this.chart.data.datasets[0].data = this.getDoughnutData(); + this.chart.update(); } + } + + ngOnInit(): void { + this.totalExecutions = this.jobExecutions.length; + + const data = { + labels: this.getDoughnutLabels(), + datasets: [ + { + data: this.getDoughnutData(), + backgroundColor: this.getDoughnutLabelColors(), + hoverOffset: 4, + }, + ], + }; + + this.chart = new Chart("statusChart", { + type: "doughnut", + data, + options: { + spacing: 1, + elements: { + arc: { + borderWidth: 0, + }, + }, + cutout: 70, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + position: "left", + }, + datalabels: { + color: "black", + font: { + size: 16, + }, + }, + tooltip: { + xAlign: "center", + yAlign: "center", + }, + }, + }, + }); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/index.ts index 6e38bf3dcf..d6916f862e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-status-chart/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './execution-status-chart.component'; +export * from "./execution-status-chart.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/index.ts index b80bfccd2a..08401470c3 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/index.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-deployment-details-modal'; -export * from './data-job-execution-status'; -export * from './data-job-execution-status-filter'; -export * from './data-job-execution-type'; -export * from './data-job-execution-type-filter'; -export * from './data-job-executions-grid'; -export * from './data-job-executions-page'; -export * from './execution-duration-chart'; -export * from './execution-status-chart'; -export * from './model'; -export * from './time-period-filter'; +export * from "./data-job-deployment-details-modal"; +export * from "./data-job-execution-status"; +export * from "./data-job-execution-status-filter"; +export * from "./data-job-execution-type"; +export * from "./data-job-execution-type-filter"; +export * from "./data-job-executions-grid"; +export * from "./data-job-executions-page"; +export * from "./execution-duration-chart"; +export * from "./execution-status-chart"; +export * from "./model"; +export * from "./time-period-filter"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.spec.ts index a556070941..f3dfa150a3 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.spec.ts @@ -3,174 +3,214 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; +import { DatePipe } from "@angular/common"; -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; -import { DataJobExecution, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../model'; +import { + DataJobExecution, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../../../../model"; -import { DataJobExecutionToGridDataJobExecution, GridDataJobExecution } from './data-job-execution'; +import { + DataJobExecutionToGridDataJobExecution, + GridDataJobExecution, +} from "./data-job-execution"; -describe('DataJobExecutionToGridDataJobExecution', () => { - let datePipe: { transform: (...args: any[]) => string }; +describe("DataJobExecutionToGridDataJobExecution", () => { + let datePipe: { transform: (...args: any[]) => string }; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe], + }); + + datePipe = TestBed.inject(DatePipe); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|convertStatus|", () => { + const params: Array<[string, string, DataJobExecutionStatus]> = [ + [ + `${DataJobExecutionStatus.SUCCEEDED}`, + null, + DataJobExecutionStatus.SUCCEEDED, + ], + [ + `${DataJobExecutionStatus.FINISHED}`, + null, + DataJobExecutionStatus.SUCCEEDED, + ], + [ + `${DataJobExecutionStatus.FAILED}`, + "Platform error", + DataJobExecutionStatus.PLATFORM_ERROR, + ], + [ + `${DataJobExecutionStatus.FAILED}`, + "Some exception message", + DataJobExecutionStatus.USER_ERROR, + ], + [ + `${DataJobExecutionStatus.FAILED}`, + null, + DataJobExecutionStatus.FAILED, + ], + [ + `Some new execution status`, + null, + `Some new execution status` as any, + ], + ]; + + for (const [status, message, assertion] of params) { + it(`should verify for provided status "${status}" will return "${assertion}"`, () => { + // When + const returnedStatus = + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + DataJobExecutionToGridDataJobExecution.convertStatus( + status as any, + message, + ); + + // Then + expect(returnedStatus).toEqual(assertion); + }); + } + }); + + describe("|convertToDataJobExecution|", () => { + it("should verify will convert object correctly", () => { + // Given + spyOn(Date, "now").and.returnValue(1608036156085); + let counter = 0; + spyOn(datePipe, "transform").and.callFake(() => { + if (++counter % 2 === 1) { + return "Dec 12, 2020, 10:10:10 AM"; + } + + return "Dec 12, 2020, 11:11:11 AM"; + }); + + /* eslint-disable @typescript-eslint/naming-convention */ + const dataJobExecution: DataJobExecution = { + jobName: "test-job", + status: DataJobExecutionStatus.FAILED, + message: "Platform error", + type: DataJobExecutionType.MANUAL, + id: "123123123", + opId: "123123123", + logsUrl: "https://logs.com", + startedBy: "pmitev", + startTime: "2020-12-12T10:10:10Z", + endTime: "2020-12-12T11:11:11Z", + deployment: { + id: "test", + jobVersion: "jkdfgh", + mode: "release", + vdkVersion: "0.0.1", + deployedDate: "2020-11-11T10:10:10Z", + deployedBy: "pmitev", + resources: { + memoryLimit: 1000, + memoryRequest: 1000, + cpuLimit: 0.5, + cpuRequest: 0.5, + }, + enabled: true, + schedule: { scheduleCron: "12 * * * *" }, + }, + }; + const expectedObject: GridDataJobExecution = { + jobName: "test-job", + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL, + startedBy: "pmitev", + startTime: "2020-12-12T10:10:10Z", + startTimeFormatted: "Dec 12, 2020, 10:10:10 AM", + endTime: "2020-12-12T11:11:11Z", + endTimeFormatted: "Dec 12, 2020, 11:11:11 AM", + duration: "1h 1m", + message: "Platform error", + id: "123123123", + opId: "123123123", + jobVersion: "jkdfgh", + logsUrl: "https://logs.com", + deployment: { + id: "test", + jobVersion: "jkdfgh", + mode: "release", + vdkVersion: "0.0.1", + deployedDate: "2020-11-11T10:10:10Z", + deployedBy: "pmitev", + resources: { + memoryLimit: 1000, + memoryRequest: 1000, + cpuLimit: 0.5, + cpuRequest: 0.5, + }, + enabled: true, + schedule: { scheduleCron: "12 * * * *" }, + }, + }; + /* eslint-enable @typescript-eslint/naming-convention */ + + const convertedJobExecution = + DataJobExecutionToGridDataJobExecution.convertToDataJobExecution( + datePipe as DatePipe, + )([ + dataJobExecution, + { + ...dataJobExecution, + endTime: null, + }, + ]); + + expect(convertedJobExecution.length).toEqual(2); + expect(convertedJobExecution).toEqual([ + expectedObject, + { + ...expectedObject, + endTimeFormatted: "", + endTime: null, + duration: "3d 2h", + }, + ]); }); + }); + + describe("|getStatusColorsMap|", () => { + it("should verify will return correct values", () => { + // When + const value = + DataJobExecutionToGridDataJobExecution.getStatusColorsMap(); + + // Then + expect(value).toEqual({ + [DataJobExecutionStatus.SUBMITTED]: "#CCCCCC", + [DataJobExecutionStatus.RUNNING]: "#CCCCCC", + [DataJobExecutionStatus.SUCCEEDED]: "#5EB715", + [DataJobExecutionStatus.CANCELLED]: "#CCCCCC", + [DataJobExecutionStatus.SKIPPED]: "#CCCCCC", + [DataJobExecutionStatus.USER_ERROR]: "#F27963", + [DataJobExecutionStatus.PLATFORM_ERROR]: "#F8CF2A", + }); + }); + }); - datePipe = TestBed.inject(DatePipe); - }); + describe("|resolveColor|", () => { + it("should verify will resolve color from map", () => { + // When + const color = DataJobExecutionToGridDataJobExecution.resolveColor( + DataJobExecutionStatus.SUCCEEDED, + ); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|convertStatus|', () => { - const params: Array<[string, string, DataJobExecutionStatus]> = [ - [`${DataJobExecutionStatus.SUCCEEDED}`, null, DataJobExecutionStatus.SUCCEEDED], - [`${DataJobExecutionStatus.FINISHED}`, null, DataJobExecutionStatus.SUCCEEDED], - [`${DataJobExecutionStatus.FAILED}`, 'Platform error', DataJobExecutionStatus.PLATFORM_ERROR], - [`${DataJobExecutionStatus.FAILED}`, 'Some exception message', DataJobExecutionStatus.USER_ERROR], - [`${DataJobExecutionStatus.FAILED}`, null, DataJobExecutionStatus.FAILED], - [`Some new execution status`, null, `Some new execution status` as any] - ]; - - for (const [status, message, assertion] of params) { - it(`should verify for provided status "${status}" will return "${assertion}"`, () => { - // When - const returnedStatus = - /* eslint-disable @typescript-eslint/no-unsafe-argument */ - DataJobExecutionToGridDataJobExecution.convertStatus(status as any, message); - - // Then - expect(returnedStatus).toEqual(assertion); - }); - } - }); - - describe('|convertToDataJobExecution|', () => { - it('should verify will convert object correctly', () => { - // Given - spyOn(Date, 'now').and.returnValue(1608036156085); - let counter = 0; - spyOn(datePipe, 'transform').and.callFake(() => { - if (++counter % 2 === 1) { - return 'Dec 12, 2020, 10:10:10 AM'; - } - - return 'Dec 12, 2020, 11:11:11 AM'; - }); - - /* eslint-disable @typescript-eslint/naming-convention */ - const dataJobExecution: DataJobExecution = { - jobName: 'test-job', - status: DataJobExecutionStatus.FAILED, - message: 'Platform error', - type: DataJobExecutionType.MANUAL, - id: '123123123', - opId: '123123123', - logsUrl: 'https://logs.com', - startedBy: 'pmitev', - startTime: '2020-12-12T10:10:10Z', - endTime: '2020-12-12T11:11:11Z', - deployment: { - id: 'test', - jobVersion: 'jkdfgh', - mode: 'release', - vdkVersion: '0.0.1', - deployedDate: '2020-11-11T10:10:10Z', - deployedBy: 'pmitev', - resources: { - memoryLimit: 1000, - memoryRequest: 1000, - cpuLimit: 0.5, - cpuRequest: 0.5 - }, - enabled: true, - schedule: { scheduleCron: '12 * * * *' } - } - }; - const expectedObject: GridDataJobExecution = { - jobName: 'test-job', - status: DataJobExecutionStatus.PLATFORM_ERROR, - type: DataJobExecutionType.MANUAL, - startedBy: 'pmitev', - startTime: '2020-12-12T10:10:10Z', - startTimeFormatted: 'Dec 12, 2020, 10:10:10 AM', - endTime: '2020-12-12T11:11:11Z', - endTimeFormatted: 'Dec 12, 2020, 11:11:11 AM', - duration: '1h 1m', - message: 'Platform error', - id: '123123123', - opId: '123123123', - jobVersion: 'jkdfgh', - logsUrl: 'https://logs.com', - deployment: { - id: 'test', - jobVersion: 'jkdfgh', - mode: 'release', - vdkVersion: '0.0.1', - deployedDate: '2020-11-11T10:10:10Z', - deployedBy: 'pmitev', - resources: { - memoryLimit: 1000, - memoryRequest: 1000, - cpuLimit: 0.5, - cpuRequest: 0.5 - }, - enabled: true, - schedule: { scheduleCron: '12 * * * *' } - } - }; - /* eslint-enable @typescript-eslint/naming-convention */ - - const convertedJobExecution = DataJobExecutionToGridDataJobExecution.convertToDataJobExecution(datePipe as DatePipe)([ - dataJobExecution, - { - ...dataJobExecution, - endTime: null - } - ]); - - expect(convertedJobExecution.length).toEqual(2); - expect(convertedJobExecution).toEqual([ - expectedObject, - { - ...expectedObject, - endTimeFormatted: '', - endTime: null, - duration: '3d 2h' - } - ]); - }); - }); - - describe('|getStatusColorsMap|', () => { - it('should verify will return correct values', () => { - // When - const value = DataJobExecutionToGridDataJobExecution.getStatusColorsMap(); - - // Then - expect(value).toEqual({ - [DataJobExecutionStatus.SUBMITTED]: '#CCCCCC', - [DataJobExecutionStatus.RUNNING]: '#CCCCCC', - [DataJobExecutionStatus.SUCCEEDED]: '#5EB715', - [DataJobExecutionStatus.CANCELLED]: '#CCCCCC', - [DataJobExecutionStatus.SKIPPED]: '#CCCCCC', - [DataJobExecutionStatus.USER_ERROR]: '#F27963', - [DataJobExecutionStatus.PLATFORM_ERROR]: '#F8CF2A' - }); - }); - }); - - describe('|resolveColor|', () => { - it('should verify will resolve color from map', () => { - // When - const color = DataJobExecutionToGridDataJobExecution.resolveColor(DataJobExecutionStatus.SUCCEEDED); - - // Then - expect(color).toEqual('#5EB715'); - }); - }); + // Then + expect(color).toEqual("#5EB715"); }); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.ts index 9344cc590b..b3ce40024d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/data-job-execution.ts @@ -3,81 +3,104 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe } from '@angular/common'; +import { DatePipe } from "@angular/common"; -import { FormatDeltaPipe } from '../../../../../shared/pipes'; +import { FormatDeltaPipe } from "../../../../../shared/pipes"; -import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecution, DataJobExecutions, DataJobExecutionStatus } from '../../../../../model'; +import { + DATA_PIPELINES_DATE_TIME_FORMAT, + DataJobExecution, + DataJobExecutions, + DataJobExecutionStatus, +} from "../../../../../model"; export interface GridDataJobExecution extends DataJobExecution { - duration: string; - jobVersion: string; - startTimeFormatted: string; - endTimeFormatted: string; + duration: string; + jobVersion: string; + startTimeFormatted: string; + endTimeFormatted: string; } export class DataJobExecutionToGridDataJobExecution { - static convertStatus(jobStatus: DataJobExecutionStatus, message: string): DataJobExecutionStatus { - switch (`${jobStatus}`.toUpperCase()) { - case DataJobExecutionStatus.SUCCEEDED: - case DataJobExecutionStatus.FINISHED: - return DataJobExecutionStatus.SUCCEEDED; - case DataJobExecutionStatus.FAILED: - if (message) { - return message === 'Platform error' ? DataJobExecutionStatus.PLATFORM_ERROR : DataJobExecutionStatus.USER_ERROR; - } else { - return DataJobExecutionStatus.FAILED; - } - default: - return jobStatus; + static convertStatus( + jobStatus: DataJobExecutionStatus, + message: string, + ): DataJobExecutionStatus { + switch (`${jobStatus}`.toUpperCase()) { + case DataJobExecutionStatus.SUCCEEDED: + case DataJobExecutionStatus.FINISHED: + return DataJobExecutionStatus.SUCCEEDED; + case DataJobExecutionStatus.FAILED: + if (message) { + return message === "Platform error" + ? DataJobExecutionStatus.PLATFORM_ERROR + : DataJobExecutionStatus.USER_ERROR; + } else { + return DataJobExecutionStatus.FAILED; } + default: + return jobStatus; } + } - static convertToDataJobExecution(datePipe: DatePipe) { - return (dataJobExecution: DataJobExecutions): GridDataJobExecution[] => { - const formatDeltaPipe = new FormatDeltaPipe(); + static convertToDataJobExecution(datePipe: DatePipe) { + return (dataJobExecution: DataJobExecutions): GridDataJobExecution[] => { + const formatDeltaPipe = new FormatDeltaPipe(); - return dataJobExecution.reduce((accumulator, execution) => { - accumulator.push({ - status: DataJobExecutionToGridDataJobExecution.convertStatus(execution.status, execution.message), - type: execution.type, - duration: formatDeltaPipe.transform(execution), - startTime: execution.startTime, - startTimeFormatted: execution.startTime - ? datePipe.transform(execution.startTime, DATA_PIPELINES_DATE_TIME_FORMAT, 'UTC') - : '', - endTime: execution.endTime ? execution.endTime : null, - endTimeFormatted: execution.endTime - ? datePipe.transform(execution.endTime, DATA_PIPELINES_DATE_TIME_FORMAT, 'UTC') - : '', - logsUrl: execution.logsUrl, - startedBy: execution.startedBy, - id: execution.id, - jobName: execution.jobName, - opId: execution.opId, - jobVersion: execution.deployment.jobVersion, - deployment: execution.deployment, - message: execution.message - }); + return dataJobExecution.reduce((accumulator, execution) => { + accumulator.push({ + status: DataJobExecutionToGridDataJobExecution.convertStatus( + execution.status, + execution.message, + ), + type: execution.type, + duration: formatDeltaPipe.transform(execution), + startTime: execution.startTime, + startTimeFormatted: execution.startTime + ? datePipe.transform( + execution.startTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + "UTC", + ) + : "", + endTime: execution.endTime ? execution.endTime : null, + endTimeFormatted: execution.endTime + ? datePipe.transform( + execution.endTime, + DATA_PIPELINES_DATE_TIME_FORMAT, + "UTC", + ) + : "", + logsUrl: execution.logsUrl, + startedBy: execution.startedBy, + id: execution.id, + jobName: execution.jobName, + opId: execution.opId, + jobVersion: execution.deployment.jobVersion, + deployment: execution.deployment, + message: execution.message, + }); - return accumulator; - }, [] as GridDataJobExecution[]); - }; - } + return accumulator; + }, [] as GridDataJobExecution[]); + }; + } - static getStatusColorsMap() { - return { - [DataJobExecutionStatus.SUBMITTED]: '#CCCCCC', - [DataJobExecutionStatus.RUNNING]: '#CCCCCC', - [DataJobExecutionStatus.SUCCEEDED]: '#5EB715', - [DataJobExecutionStatus.CANCELLED]: '#CCCCCC', - [DataJobExecutionStatus.SKIPPED]: '#CCCCCC', - [DataJobExecutionStatus.USER_ERROR]: '#F27963', - [DataJobExecutionStatus.PLATFORM_ERROR]: '#F8CF2A' - }; - } + static getStatusColorsMap() { + return { + [DataJobExecutionStatus.SUBMITTED]: "#CCCCCC", + [DataJobExecutionStatus.RUNNING]: "#CCCCCC", + [DataJobExecutionStatus.SUCCEEDED]: "#5EB715", + [DataJobExecutionStatus.CANCELLED]: "#CCCCCC", + [DataJobExecutionStatus.SKIPPED]: "#CCCCCC", + [DataJobExecutionStatus.USER_ERROR]: "#F27963", + [DataJobExecutionStatus.PLATFORM_ERROR]: "#F8CF2A", + }; + } - static resolveColor(key: string): string { - return DataJobExecutionToGridDataJobExecution.getStatusColorsMap()[key] as string; - } + static resolveColor(key: string): string { + return DataJobExecutionToGridDataJobExecution.getStatusColorsMap()[ + key + ] as string; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.spec.ts index 9bb3e45ae4..92749b08b6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.spec.ts @@ -4,52 +4,52 @@ */ import { - FILTER_DURATION_KEY, - FILTER_END_TIME_KEY, - FILTER_ID_KEY, - FILTER_START_TIME_KEY, - FILTER_STATUS_KEY, - FILTER_TIME_PERIOD_KEY, - FILTER_TYPE_KEY, - FILTER_VERSION_KEY, - SORT_DURATION_KEY, - SORT_END_TIME_KEY, - SORT_ID_KEY, - SORT_START_TIME_KEY, - SORT_STATUS_KEY, - SORT_TYPE_KEY, - SORT_VERSION_KEY, - SUPPORTED_EXECUTIONS_FILTER_CRITERIA, - SUPPORTED_EXECUTIONS_SORT_CRITERIA -} from './executions-filters.model'; + FILTER_DURATION_KEY, + FILTER_END_TIME_KEY, + FILTER_ID_KEY, + FILTER_START_TIME_KEY, + FILTER_STATUS_KEY, + FILTER_TIME_PERIOD_KEY, + FILTER_TYPE_KEY, + FILTER_VERSION_KEY, + SORT_DURATION_KEY, + SORT_END_TIME_KEY, + SORT_ID_KEY, + SORT_START_TIME_KEY, + SORT_STATUS_KEY, + SORT_TYPE_KEY, + SORT_VERSION_KEY, + SUPPORTED_EXECUTIONS_FILTER_CRITERIA, + SUPPORTED_EXECUTIONS_SORT_CRITERIA, +} from "./executions-filters.model"; -describe('SUPPORTED_EXECUTIONS_FILTER_CRITERIA', () => { - it('should verify supported filter criteria are correct', () => { - // Then - expect(SUPPORTED_EXECUTIONS_FILTER_CRITERIA).toEqual([ - FILTER_TIME_PERIOD_KEY, - FILTER_STATUS_KEY, - FILTER_TYPE_KEY, - FILTER_DURATION_KEY, - FILTER_START_TIME_KEY, - FILTER_END_TIME_KEY, - FILTER_ID_KEY, - FILTER_VERSION_KEY - ]); - }); +describe("SUPPORTED_EXECUTIONS_FILTER_CRITERIA", () => { + it("should verify supported filter criteria are correct", () => { + // Then + expect(SUPPORTED_EXECUTIONS_FILTER_CRITERIA).toEqual([ + FILTER_TIME_PERIOD_KEY, + FILTER_STATUS_KEY, + FILTER_TYPE_KEY, + FILTER_DURATION_KEY, + FILTER_START_TIME_KEY, + FILTER_END_TIME_KEY, + FILTER_ID_KEY, + FILTER_VERSION_KEY, + ]); + }); }); -describe('SUPPORTED_EXECUTIONS_SORT_CRITERIA', () => { - it('should verify supported sort criteria are correct', () => { - // Then - expect(SUPPORTED_EXECUTIONS_SORT_CRITERIA).toEqual([ - SORT_STATUS_KEY, - SORT_TYPE_KEY, - SORT_DURATION_KEY, - SORT_START_TIME_KEY, - SORT_END_TIME_KEY, - SORT_ID_KEY, - SORT_VERSION_KEY - ]); - }); +describe("SUPPORTED_EXECUTIONS_SORT_CRITERIA", () => { + it("should verify supported sort criteria are correct", () => { + // Then + expect(SUPPORTED_EXECUTIONS_SORT_CRITERIA).toEqual([ + SORT_STATUS_KEY, + SORT_TYPE_KEY, + SORT_DURATION_KEY, + SORT_START_TIME_KEY, + SORT_END_TIME_KEY, + SORT_ID_KEY, + SORT_VERSION_KEY, + ]); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts index 1dfe18b466..3554ec0eb8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts @@ -3,46 +3,49 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ClrDatagridSortOrder } from '@clr/angular'; +import { ClrDatagridSortOrder } from "@clr/angular"; -import { FILTER_KEY, KeyValueTuple, SORT_KEY } from '../../../../../commons'; +import { FILTER_KEY, KeyValueTuple, SORT_KEY } from "../../../../../commons"; -export const FILTER_TIME_PERIOD_KEY = 'timePeriod'; -export const FILTER_STATUS_KEY = 'status'; -export const FILTER_TYPE_KEY = 'type'; -export const FILTER_DURATION_KEY = 'duration'; -export const FILTER_START_TIME_KEY = 'startTime'; -export const FILTER_END_TIME_KEY = 'endTime'; -export const FILTER_ID_KEY = 'id'; -export const FILTER_VERSION_KEY = 'jobVersion'; +export const FILTER_TIME_PERIOD_KEY = "timePeriod"; +export const FILTER_STATUS_KEY = "status"; +export const FILTER_TYPE_KEY = "type"; +export const FILTER_DURATION_KEY = "duration"; +export const FILTER_START_TIME_KEY = "startTime"; +export const FILTER_END_TIME_KEY = "endTime"; +export const FILTER_ID_KEY = "id"; +export const FILTER_VERSION_KEY = "jobVersion"; /** * ** Executions supported filter criteria types. */ export type ExecutionsFilterCriteria = - | typeof FILTER_TIME_PERIOD_KEY - | typeof FILTER_STATUS_KEY - | typeof FILTER_TYPE_KEY - | typeof FILTER_DURATION_KEY - | typeof FILTER_START_TIME_KEY - | typeof FILTER_END_TIME_KEY - | typeof FILTER_ID_KEY - | typeof FILTER_VERSION_KEY; + | typeof FILTER_TIME_PERIOD_KEY + | typeof FILTER_STATUS_KEY + | typeof FILTER_TYPE_KEY + | typeof FILTER_DURATION_KEY + | typeof FILTER_START_TIME_KEY + | typeof FILTER_END_TIME_KEY + | typeof FILTER_ID_KEY + | typeof FILTER_VERSION_KEY; /** * ** Executions filter pair with its corresponding value in Tuple. */ -export type ExecutionsFilterPairs = KeyValueTuple; +export type ExecutionsFilterPairs = + KeyValueTuple; /** * ** Executions grid filter with its value. */ -export type ExecutionsGridFilter = { property: K; value: string }; +export type ExecutionsGridFilter = + { property: K; value: string }; /** * ** Executions supported filter criteria. */ -export const SUPPORTED_EXECUTIONS_FILTER_CRITERIA: ExecutionsFilterCriteria[] = [ +export const SUPPORTED_EXECUTIONS_FILTER_CRITERIA: ExecutionsFilterCriteria[] = + [ FILTER_TIME_PERIOD_KEY, FILTER_STATUS_KEY, FILTER_TYPE_KEY, @@ -50,48 +53,54 @@ export const SUPPORTED_EXECUTIONS_FILTER_CRITERIA: ExecutionsFilterCriteria[] = FILTER_START_TIME_KEY, FILTER_END_TIME_KEY, FILTER_ID_KEY, - FILTER_VERSION_KEY -]; + FILTER_VERSION_KEY, + ]; -export const SORT_STATUS_KEY = 'status'; -export const SORT_TYPE_KEY = 'type'; -export const SORT_DURATION_KEY = 'duration'; -export const SORT_START_TIME_KEY = 'startTime'; -export const SORT_END_TIME_KEY = 'endTime'; -export const SORT_ID_KEY = 'id'; -export const SORT_VERSION_KEY = 'jobVersion'; +export const SORT_STATUS_KEY = "status"; +export const SORT_TYPE_KEY = "type"; +export const SORT_DURATION_KEY = "duration"; +export const SORT_START_TIME_KEY = "startTime"; +export const SORT_END_TIME_KEY = "endTime"; +export const SORT_ID_KEY = "id"; +export const SORT_VERSION_KEY = "jobVersion"; /** * ** Executions supported sort criteria types. */ export type ExecutionsSortCriteria = - | typeof SORT_STATUS_KEY - | typeof SORT_TYPE_KEY - | typeof SORT_DURATION_KEY - | typeof SORT_START_TIME_KEY - | typeof SORT_END_TIME_KEY - | typeof SORT_ID_KEY - | typeof SORT_VERSION_KEY; + | typeof SORT_STATUS_KEY + | typeof SORT_TYPE_KEY + | typeof SORT_DURATION_KEY + | typeof SORT_START_TIME_KEY + | typeof SORT_END_TIME_KEY + | typeof SORT_ID_KEY + | typeof SORT_VERSION_KEY; /** * ** Executions sort pair with its corresponding value in Tuple. */ -export type ExecutionsSortPairs = KeyValueTuple; +export type ExecutionsSortPairs = KeyValueTuple< + ExecutionsSortCriteria, + ClrDatagridSortOrder +>; /** * ** Executions supported sort criteria. */ export const SUPPORTED_EXECUTIONS_SORT_CRITERIA: ExecutionsSortCriteria[] = [ - SORT_STATUS_KEY, - SORT_TYPE_KEY, - SORT_DURATION_KEY, - SORT_START_TIME_KEY, - SORT_END_TIME_KEY, - SORT_ID_KEY, - SORT_VERSION_KEY + SORT_STATUS_KEY, + SORT_TYPE_KEY, + SORT_DURATION_KEY, + SORT_START_TIME_KEY, + SORT_END_TIME_KEY, + SORT_ID_KEY, + SORT_VERSION_KEY, ]; /** * ** Executions object that holds filter and sort criteria and their corresponding values. */ -export type ExecutionsFilterSortObject = Record; +export type ExecutionsFilterSortObject = Record< + typeof FILTER_KEY | typeof SORT_KEY, + string +>; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/index.ts index 48ccedfbe8..9933269835 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-execution'; -export * from './executions-filters.model'; +export * from "./data-job-execution"; +export * from "./executions-filters.model"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/public-api.ts index a5110c341c..08c8aed979 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-executions-page'; +export * from "./data-job-executions-page"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/index.ts index 8642886088..0292983f5a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './time-period-filter.component'; +export * from "./time-period-filter.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html index 00870bbcf3..15de162f37 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html @@ -4,170 +4,141 @@ -->
- Filter by time period (UTC): - - - + Filter by time period (UTC): + + +
- -

- {{ fromDateTime | date : dateTimeFormat : "UTC" }} to {{ toDateTime - | date : dateTimeFormat : "UTC" }} - -

- -
+

+ {{ fromDateTime | date: dateTimeFormat : "UTC" }} to + {{ toDateTime | date: dateTimeFormat : "UTC" }} + +

+ + +
+ +
+
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ -
-
-
-
-
- -
-
-
- - -
-
-
-
-
-
- - -
-
- - - + Reset + + +
+ + + + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.scss index 2e828e35bc..0013a22662 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.scss @@ -4,306 +4,318 @@ */ :host { - .data-pipelines-job__executions-period-filter-caret { - margin-top: -2px; + .data-pipelines-job__executions-period-filter-caret { + margin-top: -2px; + } + .time-filter__action-btn { + display: flex; + justify-content: flex-end; + + button { + height: 1.6rem; + line-height: 1.6rem; + margin: 0; + + &:first-child { + margin-right: 0.6rem; + } } - .time-filter__action-btn { - display: flex; - justify-content: flex-end; - - button { - height: 1.6rem; - line-height: 1.6rem; - margin: 0; - + } + + .time-filter__heading { + font-weight: bold; + margin-right: 10px; + } + + .time-filter__container { + display: flex; + align-items: center; + } + + .time-filter__label { + line-height: 1rem; + } + + ::ng-deep { + clr-signpost { + p { + color: #0a7bd8; + margin: 0; + } + + form { + width: 13rem; + + .clr-date-container { + &.clr-form-control { &:first-child { - margin-right: 0.6rem; + margin-top: 0; } + } } - } + } - .time-filter__heading { - font-weight: bold; - margin-right: 10px; + .signpost-content-body { + overflow-y: visible; + } } - .time-filter__container { - display: flex; - align-items: center; - } + dp-date-picker { + --date-picker-border-color: rgba(0, 0, 0, 0.1); + --date-picker-box-shadow: 1px 1px 5px #0000001a; + + --date-picker-background-color: #fff; + --date-picker-selected-background-color: var( + --clr-calendar-active-cell-background-color, + #d8e3e9 + ); + --date-picker-hover-background-color: var( + --clr-calendar-btn-hover-focus-color, + #e8e8e8 + ); + --date-picker-text-color: var(--clr-p1-color, #666666); + --date-picker-selected-text-color: var( + --clr-calendar-active-cell-color, + black + ); + --date-picker-current-text-color: var( + --clr-calendar-today-date-cell-color, + black + ); + + .dp-popup { + background: var(--date-picker-background-color); + border-color: var(--date-picker-border-color); + box-shadow: var(--date-picker-box-shadow); + } + + dp-calendar-nav { + &.dp-material { + .dp-calendar-nav-container { + border: 0; + height: 36px; + } + + .dp-nav-btns-container { + right: 0; + } + + .dp-nav-header { + left: 10px; + } + + .dp-nav-header-btn { + height: 1.8rem; + padding: 0; + margin: 0; + border: 0; + width: auto; + font-size: var(--clr-calendar-picker-btn-font-size, 0.9rem); + font-weight: var(--clr-calendar-picker-btn-font-weight, 200); + line-height: 1.8rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + color: var(--clr-calendar-btn-color, #0072a3); + background: var(--date-picker-background-color); + + &:hover { + background: var(--date-picker-hover-background-color); + } + } - .time-filter__label { - line-height: 1rem; - } + .dp-calendar-nav-left { + &:before { + left: 2px; + } + } - ::ng-deep { - clr-signpost { - p { - color: #0a7bd8; - margin: 0; + .dp-calendar-nav-right { + &:before { + left: -2px; + } + } + + .dp-calendar-nav-left, + .dp-calendar-nav-right { + width: 36px; + height: 36px; + color: var(--clr-calendar-btn-color, #0072a3); + background: var(--date-picker-background-color); + + &:before { + width: 10px; + height: 10px; } - form { - width: 13rem; + &:hover { + background: var(--date-picker-hover-background-color); + } + } - .clr-date-container { - &.clr-form-control { - &:first-child { - margin-top: 0; - } - } - } + .dp-current-location-btn { + background: var(--clr-calendar-btn-color, #0072a3); + width: 18px; + height: 18px; + } + } + } + + dp-day-calendar { + &.dp-material { + .dp-calendar-wrapper { + border: 0; + } + + .dp-weekdays { + margin-bottom: 0; + height: 36px; + display: flex; + align-items: center; + } + + .dp-calendar-weekday { + width: 36px; + height: 25px; + text-align: center; + font-size: var(--clr-day-font-size, 0.6rem); + font-weight: 600; + color: var(--clr-p1-color, #666666); + } + + .dp-calendar-day { + height: 1.8rem; + min-width: 1.8rem; + line-height: 1.8rem; + padding: 0; + margin: 0; + border: 0; + border-radius: var(--clr-global-borderradius, 0.15rem); + color: var(--date-picker-text-color); + background: var(--date-picker-background-color); + + &.dp-current-day { + color: var(--date-picker-current-text-color); + font-weight: var(--clr-calendar-today-date-cell-font-weight, 600); } - .signpost-content-body { - overflow-y: visible; + &.dp-selected { + color: var(--date-picker-selected-text-color); + background: var(--date-picker-selected-background-color); } + + &:hover { + background: var(--date-picker-hover-background-color); + } + + &:disabled { + opacity: 0.4; + pointer-events: none; + } + } } - dp-date-picker { - --date-picker-border-color: rgba(0, 0, 0, 0.1); - --date-picker-box-shadow: 1px 1px 5px #0000001a; - - --date-picker-background-color: #fff; - --date-picker-selected-background-color: var(--clr-calendar-active-cell-background-color, #d8e3e9); - --date-picker-hover-background-color: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); - --date-picker-text-color: var(--clr-p1-color, #666666); - --date-picker-selected-text-color: var(--clr-calendar-active-cell-color, black); - --date-picker-current-text-color: var(--clr-calendar-today-date-cell-color, black); - - .dp-popup { - background: var(--date-picker-background-color); - border-color: var(--date-picker-border-color); - box-shadow: var(--date-picker-box-shadow); + .dp-day-calendar-container { + background: var(--date-picker-background-color); + } + } + + dp-day-time-calendar { + padding: 0.6rem; + + &.dp-material { + dp-time-select { + border: 0; + + .dp-time-select-controls { + background: var(--date-picker-background-color); + } + + .dp-time-select-control { + width: 36px; + + &.dp-time-select-separator { + width: 5px; + } + } + + .dp-time-select-control-up { + &:before { + top: 2px; + } } - dp-calendar-nav { - &.dp-material { - .dp-calendar-nav-container { - border: 0; - height: 36px; - } - - .dp-nav-btns-container { - right: 0; - } - - .dp-nav-header { - left: 10px; - } - - .dp-nav-header-btn { - height: 1.8rem; - padding: 0; - margin: 0; - border: 0; - width: auto; - font-size: var(--clr-calendar-picker-btn-font-size, 0.9rem); - font-weight: var(--clr-calendar-picker-btn-font-weight, 200); - line-height: 1.8rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - color: var(--clr-calendar-btn-color, #0072a3); - background: var(--date-picker-background-color); - - &:hover { - background: var(--date-picker-hover-background-color); - } - } - - .dp-calendar-nav-left { - &:before { - left: 2px; - } - } - - .dp-calendar-nav-right { - &:before { - left: -2px; - } - } - - .dp-calendar-nav-left, - .dp-calendar-nav-right { - width: 36px; - height: 36px; - color: var(--clr-calendar-btn-color, #0072a3); - background: var(--date-picker-background-color); - - &:before { - width: 10px; - height: 10px; - } - - &:hover { - background: var(--date-picker-hover-background-color); - } - } - - .dp-current-location-btn { - background: var(--clr-calendar-btn-color, #0072a3); - width: 18px; - height: 18px; - } - } + .dp-time-select-control-down { + &:before { + top: -2px; + } } - dp-day-calendar { - &.dp-material { - .dp-calendar-wrapper { - border: 0; - } - - .dp-weekdays { - margin-bottom: 0; - height: 36px; - display: flex; - align-items: center; - } - - .dp-calendar-weekday { - width: 36px; - height: 25px; - text-align: center; - font-size: var(--clr-day-font-size, 0.6rem); - font-weight: 600; - color: var(--clr-p1-color, #666666); - } - - .dp-calendar-day { - height: 1.8rem; - min-width: 1.8rem; - line-height: 1.8rem; - padding: 0; - margin: 0; - border: 0; - border-radius: var(--clr-global-borderradius, 0.15rem); - color: var(--date-picker-text-color); - background: var(--date-picker-background-color); - - &.dp-current-day { - color: var(--date-picker-current-text-color); - font-weight: var(--clr-calendar-today-date-cell-font-weight, 600); - } - - &.dp-selected { - color: var(--date-picker-selected-text-color); - background: var(--date-picker-selected-background-color); - } - - &:hover { - background: var(--date-picker-hover-background-color); - } - - &:disabled { - opacity: 0.4; - pointer-events: none; - } - } - } - - .dp-day-calendar-container { - background: var(--date-picker-background-color); - } + .dp-time-select-control-up, + .dp-time-select-control-down { + padding: 0; + margin: 0; + border: 0; + width: 36px; + height: 36px; + border-radius: var(--clr-global-borderradius, 0.15rem); + color: var(--clr-calendar-btn-color, #0072a3); + + &:before { + width: 9px; + height: 9px; + } + + &:hover { + background: var(--date-picker-hover-background-color); + } + } + } + } + } + + dp-month-calendar { + &.dp-material { + .dp-month-calendar-container { + background: var(--date-picker-background-color); + } + + .dp-calendar-month { + height: 2.4rem; + min-width: 2.4rem; + width: 6.3rem; + padding: 0 0.6rem; + margin: 0; + border: 0; + border-radius: var(--clr-global-borderradius, 0.15rem); + cursor: pointer; + font-size: 0.9rem; + font-weight: 200; + line-height: 1.8rem; + color: var(--date-picker-text-color); + background: var(--date-picker-background-color); + + &.dp-current-month { + color: var(--date-picker-current-text-color); + font-weight: var(--clr-calendar-today-date-cell-font-weight, 600); + } + + &.dp-selected { + color: var(--date-picker-selected-text-color); + background: var(--date-picker-selected-background-color); } - dp-day-time-calendar { - padding: 0.6rem; - - &.dp-material { - dp-time-select { - border: 0; - - .dp-time-select-controls { - background: var(--date-picker-background-color); - } - - .dp-time-select-control { - width: 36px; - - &.dp-time-select-separator { - width: 5px; - } - } - - .dp-time-select-control-up { - &:before { - top: 2px; - } - } - - .dp-time-select-control-down { - &:before { - top: -2px; - } - } - - .dp-time-select-control-up, - .dp-time-select-control-down { - padding: 0; - margin: 0; - border: 0; - width: 36px; - height: 36px; - border-radius: var(--clr-global-borderradius, 0.15rem); - color: var(--clr-calendar-btn-color, #0072a3); - - &:before { - width: 9px; - height: 9px; - } - - &:hover { - background: var(--date-picker-hover-background-color); - } - } - } - } + &:hover { + background: var(--date-picker-hover-background-color); } - dp-month-calendar { - &.dp-material { - .dp-month-calendar-container { - background: var(--date-picker-background-color); - } - - .dp-calendar-month { - height: 2.4rem; - min-width: 2.4rem; - width: 6.3rem; - padding: 0 0.6rem; - margin: 0; - border: 0; - border-radius: var(--clr-global-borderradius, 0.15rem); - cursor: pointer; - font-size: 0.9rem; - font-weight: 200; - line-height: 1.8rem; - color: var(--date-picker-text-color); - background: var(--date-picker-background-color); - - &.dp-current-month { - color: var(--date-picker-current-text-color); - font-weight: var(--clr-calendar-today-date-cell-font-weight, 600); - } - - &.dp-selected { - color: var(--date-picker-selected-text-color); - background: var(--date-picker-selected-background-color); - } - - &:hover { - background: var(--date-picker-hover-background-color); - } - - &:disabled { - opacity: 0.4; - pointer-events: none; - } - } - } + &:disabled { + opacity: 0.4; + pointer-events: none; } + } } + } } + } } // TODO uncomment for v13+ @@ -556,40 +568,40 @@ //} .fade-to-dark { - &.dark { - :host { - ::ng-deep { - dp-date-picker { - --date-picker-border-color: #000; - --date-picker-box-shadow: 0 0.05rem 0.15rem rgb(0 0 0 / 50%); - - --date-picker-background-color: #21333b; - --date-picker-selected-background-color: #324f62; - --date-picker-hover-background-color: #28404d; - --date-picker-text-color: #acbac3; - --date-picker-selected-text-color: #fff; - --date-picker-current-text-color: #fff; - } - } + &.dark { + :host { + ::ng-deep { + dp-date-picker { + --date-picker-border-color: #000; + --date-picker-box-shadow: 0 0.05rem 0.15rem rgb(0 0 0 / 50%); + + --date-picker-background-color: #21333b; + --date-picker-selected-background-color: #324f62; + --date-picker-hover-background-color: #28404d; + --date-picker-text-color: #acbac3; + --date-picker-selected-text-color: #fff; + --date-picker-current-text-color: #fff; } - - // TODO uncomment for v13+ - //::ng-deep .cdk-overlay-container { - // .dp-material { - // &.data-pipelines__executions-filter-picker { - // &.dp-popup { - // --date-picker-border-color: #000; - // --date-picker-box-shadow: 0 0.05rem 0.15rem rgb(0 0 0 / 50%); - // - // --date-picker-background-color: #21333b; - // --date-picker-selected-background-color: #324f62; - // --date-picker-hover-background-color: #28404d; - // --date-picker-text-color: #acbac3; - // --date-picker-selected-text-color: #fff; - // --date-picker-current-text-color: #fff; - // } - // } - // } - //} + } } + + // TODO uncomment for v13+ + //::ng-deep .cdk-overlay-container { + // .dp-material { + // &.data-pipelines__executions-filter-picker { + // &.dp-popup { + // --date-picker-border-color: #000; + // --date-picker-box-shadow: 0 0.05rem 0.15rem rgb(0 0 0 / 50%); + // + // --date-picker-background-color: #21333b; + // --date-picker-selected-background-color: #324f62; + // --date-picker-hover-background-color: #28404d; + // --date-picker-text-color: #acbac3; + // --date-picker-selected-text-color: #fff; + // --date-picker-current-text-color: #fff; + // } + // } + // } + //} + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.ts index 37de5fd76b..f67a751aa9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.ts @@ -3,576 +3,743 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; -import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; - -import { CalendarValue, DatePickerDirective, IDatePickerDirectiveConfig } from 'ng2-date-picker'; +import { + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, + ViewChild, +} from "@angular/core"; +import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; + +import { + CalendarValue, + DatePickerDirective, + IDatePickerDirectiveConfig, +} from "ng2-date-picker"; // TODO [import dayjs from 'dayjs'] used in ng2-date-picker v13+ instead of moment -import moment from 'moment'; +import moment from "moment"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { FiltersSortManager } from '../../../../../commons'; +import { FiltersSortManager } from "../../../../../commons"; -import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../model'; +import { DATA_PIPELINES_DATE_TIME_FORMAT } from "../../../../../model"; -import { ExecutionsFilterCriteria, FILTER_TIME_PERIOD_KEY } from '../model'; +import { ExecutionsFilterCriteria, FILTER_TIME_PERIOD_KEY } from "../model"; type CustomFormGroup = FormGroup & { controls: { [key: string]: FormControl } }; interface DateTimePeriod { - from: Date; - to: Date; + from: Date; + to: Date; } @Component({ - selector: 'lib-time-period-filter', - templateUrl: './time-period-filter.component.html', - styleUrls: ['./time-period-filter.component.scss'] + selector: "lib-time-period-filter", + templateUrl: "./time-period-filter.component.html", + styleUrls: ["./time-period-filter.component.scss"], }) export class TimePeriodFilterComponent implements OnInit, OnChanges, OnDestroy { - @ViewChild('fromPicker') fromPicker: DatePickerDirective; - @ViewChild('toPicker') toPicker: DatePickerDirective; - - /** - * ** Whether component is in state loading. - */ - @Input() loading = false; - - /** - * ** Flag that indicates there is jobs executions load error. - */ - @Input() isComponentInErrorState = false; - - /** - * ** Executions filters sort manager injected from parent. - */ - @Input() filtersSortManager: Readonly>; - - /** - * ** Date time period serialized in string format with pattern "{{epochDateTime}}-{{epochDateTime}}". - */ - @Input() selectedPeriodSerialized: string; - - /** - * ** Date time period in raw format of type object with field from and to of type Date. - */ - @Input() selectedPeriod: DateTimePeriod; - - /** - * ** Minimum available for selection DateTime in UTC. - */ - @Input() minDateTime: Date; - - /** - * ** Event Emitter that emits events on every user form action like submit or clear. - */ - @Output() filterChanged: EventEmitter = new EventEmitter(); - - pickerConfig: IDatePickerDirectiveConfig = { - // TODO [format: 'MMM DD, YYYY, hh:mm:ss A',] dayjs - format: 'MMM DD, yyyy, hh:mm:ss A', - showGoToCurrent: true, - showTwentyFourHours: false, - showSeconds: true, - weekDayFormat: 'dd', - numOfMonthRows: 6, - monthBtnFormat: 'MMMM' - }; - fromPickerConfig: IDatePickerDirectiveConfig = { - ...this.pickerConfig - }; - toPickerConfig: IDatePickerDirectiveConfig = { - ...this.pickerConfig - }; - - /** - * ** User selected value for "FROM" time date picker. - */ - fromDateTime: Date = null; - - /** - * ** User selected value for "TO" time date picker. - */ - toDateTime: Date = null; - - /** - * ** DateTime format pattern provided to Angular DateTime pipe. - */ - dateTimeFormat: string = DATA_PIPELINES_DATE_TIME_FORMAT; - - /** - * ** Allowed ranges for "fromDateTime" time date picker. - */ - // "FROM" time min allowed value is used from {@link this.minDateTime} - fromDateTimeMin: moment.Moment; - // "FROM" time max allowed value is less than {@link this.toDateTimeMin} - fromDateTimeMax: moment.Moment; - - /** - * ** Allowed ranges for "toDateTime" time date picker. - */ - // "TO" time min allowed value is less than {@link this.fromDateTimeMax} - toDateTimeMin: moment.Moment; - // "TO" time max allowed value for selection is current time. There is scheduled interval on 15s that updates this value. - // TODO check if will work without upper range - toDateTimeMax: moment.Moment; - - /** - * ** Angular form where DatePicker inputs belongs. - */ - tmForm: CustomFormGroup; - - private _isFromPickerOpened = false; - private _isToPickerOpened = false; - - private _initiallySetMin = false; - private _initiallySetMax = false; - - // refresh interval scheduled reference - private _refreshIntervalRef: number; - - private _previousSelectedPeriodSerialized: string; - - private _isUrlNormalized = false; - - /** - * ** Constructor. - */ - constructor(private readonly formBuilder: FormBuilder) { - this.tmForm = this.formBuilder.group({ - fromDateTime: '', - toDateTime: '' - }) as CustomFormGroup; + @ViewChild("fromPicker") fromPicker: DatePickerDirective; + @ViewChild("toPicker") toPicker: DatePickerDirective; + + /** + * ** Whether component is in state loading. + */ + @Input() loading = false; + + /** + * ** Flag that indicates there is jobs executions load error. + */ + @Input() isComponentInErrorState = false; + + /** + * ** Executions filters sort manager injected from parent. + */ + @Input() filtersSortManager: Readonly< + FiltersSortManager + >; + + /** + * ** Date time period serialized in string format with pattern "{{epochDateTime}}-{{epochDateTime}}". + */ + @Input() selectedPeriodSerialized: string; + + /** + * ** Date time period in raw format of type object with field from and to of type Date. + */ + @Input() selectedPeriod: DateTimePeriod; + + /** + * ** Minimum available for selection DateTime in UTC. + */ + @Input() minDateTime: Date; + + /** + * ** Event Emitter that emits events on every user form action like submit or clear. + */ + @Output() filterChanged: EventEmitter = + new EventEmitter(); + + pickerConfig: IDatePickerDirectiveConfig = { + // TODO [format: 'MMM DD, YYYY, hh:mm:ss A',] dayjs + format: "MMM DD, yyyy, hh:mm:ss A", + showGoToCurrent: true, + showTwentyFourHours: false, + showSeconds: true, + weekDayFormat: "dd", + numOfMonthRows: 6, + monthBtnFormat: "MMMM", + }; + fromPickerConfig: IDatePickerDirectiveConfig = { + ...this.pickerConfig, + }; + toPickerConfig: IDatePickerDirectiveConfig = { + ...this.pickerConfig, + }; + + /** + * ** User selected value for "FROM" time date picker. + */ + fromDateTime: Date = null; + + /** + * ** User selected value for "TO" time date picker. + */ + toDateTime: Date = null; + + /** + * ** DateTime format pattern provided to Angular DateTime pipe. + */ + dateTimeFormat: string = DATA_PIPELINES_DATE_TIME_FORMAT; + + /** + * ** Allowed ranges for "fromDateTime" time date picker. + */ + // "FROM" time min allowed value is used from {@link this.minDateTime} + fromDateTimeMin: moment.Moment; + // "FROM" time max allowed value is less than {@link this.toDateTimeMin} + fromDateTimeMax: moment.Moment; + + /** + * ** Allowed ranges for "toDateTime" time date picker. + */ + // "TO" time min allowed value is less than {@link this.fromDateTimeMax} + toDateTimeMin: moment.Moment; + // "TO" time max allowed value for selection is current time. There is scheduled interval on 15s that updates this value. + // TODO check if will work without upper range + toDateTimeMax: moment.Moment; + + /** + * ** Angular form where DatePicker inputs belongs. + */ + tmForm: CustomFormGroup; + + private _isFromPickerOpened = false; + private _isToPickerOpened = false; + + private _initiallySetMin = false; + private _initiallySetMax = false; + + // refresh interval scheduled reference + private _refreshIntervalRef: number; + + private _previousSelectedPeriodSerialized: string; + + private _isUrlNormalized = false; + + /** + * ** Constructor. + */ + constructor(private readonly formBuilder: FormBuilder) { + this.tmForm = this.formBuilder.group({ + fromDateTime: "", + toDateTime: "", + }) as CustomFormGroup; + } + + onDateTimeChange($event: CalendarValue, type: "from" | "to"): void { + if (CollectionsUtil.isNil($event)) { + return; } - onDateTimeChange($event: CalendarValue, type: 'from' | 'to'): void { - if (CollectionsUtil.isNil($event)) { - return; - } - - const emittedDate = $event as moment.Moment; - - if (type === 'from') { - if (emittedDate.isBefore(this.fromDateTimeMin) || emittedDate.isAfter(this.fromDateTimeMax)) { - return; - } - - this.toDateTimeMin = this._adjustDateTime(emittedDate, 'min'); - this.toPickerConfig = { - ...this.toPickerConfig, - min: this.toDateTimeMin - }; - - this.fromDateTime = new Date(this._adjustDateTime(emittedDate, 'from', 'timezone').valueOf()); - } else { - if (emittedDate.isBefore(this.toDateTimeMin) || emittedDate.isAfter(this.toDateTimeMax)) { - return; - } - - this.fromDateTimeMax = this._adjustDateTime(emittedDate, 'max'); - this.fromPickerConfig = { - ...this.fromPickerConfig, - max: this.fromDateTimeMax - }; - - this.toDateTime = new Date(this._adjustDateTime(emittedDate, 'to', 'timezone').valueOf()); - } + const emittedDate = $event as moment.Moment; + + if (type === "from") { + if ( + emittedDate.isBefore(this.fromDateTimeMin) || + emittedDate.isAfter(this.fromDateTimeMax) + ) { + return; + } + + this.toDateTimeMin = this._adjustDateTime(emittedDate, "min"); + this.toPickerConfig = { + ...this.toPickerConfig, + min: this.toDateTimeMin, + }; + + this.fromDateTime = new Date( + this._adjustDateTime(emittedDate, "from", "timezone").valueOf(), + ); + } else { + if ( + emittedDate.isBefore(this.toDateTimeMin) || + emittedDate.isAfter(this.toDateTimeMax) + ) { + return; + } + + this.fromDateTimeMax = this._adjustDateTime(emittedDate, "max"); + this.fromPickerConfig = { + ...this.fromPickerConfig, + max: this.fromDateTimeMax, + }; + + this.toDateTime = new Date( + this._adjustDateTime(emittedDate, "to", "timezone").valueOf(), + ); } - - togglePicker($event: MouseEvent, type: 'from' | 'to'): void { - $event.preventDefault(); - - if (type === 'from') { - if (this._isFromPickerOpened) { - this.fromPicker.api.close(); - } else { - this.fromPicker.api.open(); - } - } else { - if (this._isToPickerOpened) { - this.toPicker.api.close(); - } else { - this.toPicker.api.open(); - } - } + } + + togglePicker($event: MouseEvent, type: "from" | "to"): void { + $event.preventDefault(); + + if (type === "from") { + if (this._isFromPickerOpened) { + this.fromPicker.api.close(); + } else { + this.fromPicker.api.open(); + } + } else { + if (this._isToPickerOpened) { + this.toPicker.api.close(); + } else { + this.toPicker.api.open(); + } } + } - onPickerOpened(type: 'from' | 'to', isOpened: boolean): void { - if (type === 'from') { - this._isFromPickerOpened = isOpened; - } else { - this._isToPickerOpened = isOpened; - } + onPickerOpened(type: "from" | "to", isOpened: boolean): void { + if (type === "from") { + this._isFromPickerOpened = isOpened; + } else { + this._isToPickerOpened = isOpened; } + } - /** - * ** Apply selected values and emit. - */ - applyFilter($event: Event): void { - $event?.preventDefault(); + /** + * ** Apply selected values and emit. + */ + applyFilter($event: Event): void { + $event?.preventDefault(); - this._updateFiltersSortManager(); - this._emitChanges(); - } + this._updateFiltersSortManager(); + this._emitChanges(); + } - /** - * ** Clear selected values and emit. - */ - clearFilter($event: MouseEvent, triggerMinMaxDateTimeChangeDetection = false): void { - $event?.preventDefault(); + /** + * ** Clear selected values and emit. + */ + clearFilter( + $event: MouseEvent, + triggerMinMaxDateTimeChangeDetection = false, + ): void { + $event?.preventDefault(); - this._initiallySetMin = false; - this._initiallySetMax = false; + this._initiallySetMin = false; + this._initiallySetMax = false; - this.fromDateTime = null; - this.toDateTime = null; + this.fromDateTime = null; + this.toDateTime = null; - this._updateFiltersSortManager(); - this._emitChanges(); + this._updateFiltersSortManager(); + this._emitChanges(); - if (triggerMinMaxDateTimeChangeDetection) { - this._changeDetectionMinDateTime(); + if (triggerMinMaxDateTimeChangeDetection) { + this._changeDetectionMinDateTime(); - this._changeDetectionMaxDateTime(); - } + this._changeDetectionMaxDateTime(); + } + } + + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + if (changes["selectedPeriodSerialized"]) { + this._changeDetectionSelectedPeriodSerialized(); } - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges): void { - if (changes['selectedPeriodSerialized']) { - this._changeDetectionSelectedPeriodSerialized(); - } - - if (changes['selectedPeriod'] && !changes['selectedPeriod'].firstChange) { - this._changeDetectionSelectedPeriod(); - } - - if (changes['minDateTime']) { - if (CollectionsUtil.isDefined(this.minDateTime)) { - this._changeDetectionMinDateTime(); + if (changes["selectedPeriod"] && !changes["selectedPeriod"].firstChange) { + this._changeDetectionSelectedPeriod(); + } - this._changeDetectionMaxDateTime(); - } - } + if (changes["minDateTime"]) { + if (CollectionsUtil.isDefined(this.minDateTime)) { + this._changeDetectionMinDateTime(); - this._normalizeUrlAfterLoad(); + this._changeDetectionMaxDateTime(); + } } - /** - * @inheritDoc - */ - ngOnInit(): void { - // TODO check if we could not set max time for "TO" period filter and instead current time to be the max allowed - this._refreshIntervalRef = setInterval( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this._changeDetectionMaxDateTime.bind(this), - 15 * 1000 - ); // Update max time every 15s - - // TODO check if would have better performance with mutation observer - // register callback that would listen for mutation of supported filter and sort criteria - // this.filtersSortManager.registerMutationObserver((changes) => { - // const foundIndex = changes.findIndex(([key, value]) => key === FILTER_TIME_PERIOD_KEY && this._previousSelectedPeriodSerialized !== value); - // if (foundIndex !== -1) { - // this.selectedPeriodSerialized = changes[foundIndex][1] as string; - // this._changeDetectionSelectedPeriodSerialized(); - // } - // }); + this._normalizeUrlAfterLoad(); + } + + /** + * @inheritDoc + */ + ngOnInit(): void { + // TODO check if we could not set max time for "TO" period filter and instead current time to be the max allowed + this._refreshIntervalRef = setInterval( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this._changeDetectionMaxDateTime.bind(this), + 15 * 1000, + ); // Update max time every 15s + + // TODO check if would have better performance with mutation observer + // register callback that would listen for mutation of supported filter and sort criteria + // this.filtersSortManager.registerMutationObserver((changes) => { + // const foundIndex = changes.findIndex(([key, value]) => key === FILTER_TIME_PERIOD_KEY && this._previousSelectedPeriodSerialized !== value); + // if (foundIndex !== -1) { + // this.selectedPeriodSerialized = changes[foundIndex][1] as string; + // this._changeDetectionSelectedPeriodSerialized(); + // } + // }); + } + + /** + * @inheritDoc + */ + ngOnDestroy(): void { + if (this._refreshIntervalRef) { + clearInterval(this._refreshIntervalRef); + } + } + + private _adjustDateTime( + date: number | string | Date | moment.Moment, + type: "min" | "max" | "from" | "to", + travel: "utc" | "timezone" = null, + ): moment.Moment { + const offset = moment().utcOffset(); + + let dtInstance = moment(date); + + if (travel) { + if (offset > 0) { + dtInstance = + travel === "utc" + ? dtInstance.subtract(offset, "m") + : dtInstance.add(offset, "m"); + } else if (offset < 0) { + dtInstance = + travel === "utc" + ? dtInstance.add(-offset, "m") + : dtInstance.subtract(-offset, "m"); + } } - /** - * @inheritDoc - */ - ngOnDestroy(): void { - if (this._refreshIntervalRef) { - clearInterval(this._refreshIntervalRef); - } + if (type === "min") { + dtInstance = dtInstance.millisecond(0).subtract(1, "ms"); + } else if (type === "max") { + dtInstance = dtInstance.millisecond(0).add(1, "ms"); + } else if (type === "from") { + dtInstance = dtInstance.millisecond(0); + } else if (type === "to") { + dtInstance = dtInstance.millisecond(999); } - private _adjustDateTime( - date: number | string | Date | moment.Moment, - type: 'min' | 'max' | 'from' | 'to', - travel: 'utc' | 'timezone' = null - ): moment.Moment { - const offset = moment().utcOffset(); - - let dtInstance = moment(date); - - if (travel) { - if (offset > 0) { - dtInstance = travel === 'utc' ? dtInstance.subtract(offset, 'm') : dtInstance.add(offset, 'm'); - } else if (offset < 0) { - dtInstance = travel === 'utc' ? dtInstance.add(-offset, 'm') : dtInstance.subtract(-offset, 'm'); - } - } + return dtInstance; + } + + private _updateFiltersSortManager(): void { + if ( + CollectionsUtil.isNil(this.fromDateTime) || + CollectionsUtil.isNil(this.toDateTime) + ) { + this._previousSelectedPeriodSerialized = undefined; + this.filtersSortManager.deleteFilter(FILTER_TIME_PERIOD_KEY); + } else { + const serializedDateTimePeriod = + this._serializeDateTimePeriodPairValues(); + + this._previousSelectedPeriodSerialized = serializedDateTimePeriod; + this.filtersSortManager.setFilter( + FILTER_TIME_PERIOD_KEY, + serializedDateTimePeriod, + ); + } + } - if (type === 'min') { - dtInstance = dtInstance.millisecond(0).subtract(1, 'ms'); - } else if (type === 'max') { - dtInstance = dtInstance.millisecond(0).add(1, 'ms'); - } else if (type === 'from') { - dtInstance = dtInstance.millisecond(0); - } else if (type === 'to') { - dtInstance = dtInstance.millisecond(999); - } + private _changeDetectionSelectedPeriodSerialized(): void { + if ( + this._previousSelectedPeriodSerialized === this.selectedPeriodSerialized + ) { + return; + } - return dtInstance; + if ( + !CollectionsUtil.isString(this.selectedPeriodSerialized) || + this.selectedPeriodSerialized.length === 0 + ) { + if ( + CollectionsUtil.isDefined(this.fromDateTime) || + CollectionsUtil.isDefined(this.toDateTime) + ) { + this.clearFilter(null); + } + } else { + const deserializedPeriodValues: DateTimePeriod = + this._deserializeDateTimePeriodPairValues( + this.selectedPeriodSerialized, + ); + if (deserializedPeriodValues) { + this.fromDateTime = deserializedPeriodValues.from; + this.toDateTime = deserializedPeriodValues.to; + + this._updateForm("both"); + + this.applyFilter(null); + } } + } - private _updateFiltersSortManager(): void { - if (CollectionsUtil.isNil(this.fromDateTime) || CollectionsUtil.isNil(this.toDateTime)) { - this._previousSelectedPeriodSerialized = undefined; - this.filtersSortManager.deleteFilter(FILTER_TIME_PERIOD_KEY); - } else { - const serializedDateTimePeriod = this._serializeDateTimePeriodPairValues(); + private _changeDetectionSelectedPeriod(): void { + if (CollectionsUtil.isNil(this.selectedPeriod)) { + return; + } - this._previousSelectedPeriodSerialized = serializedDateTimePeriod; - this.filtersSortManager.setFilter(FILTER_TIME_PERIOD_KEY, serializedDateTimePeriod); - } + if ( + this.fromDateTime === this.selectedPeriod.from && + this.toDateTime === this.selectedPeriod.to + ) { + return; } - private _changeDetectionSelectedPeriodSerialized(): void { - if (this._previousSelectedPeriodSerialized === this.selectedPeriodSerialized) { - return; + if ( + CollectionsUtil.isNil(this.selectedPeriod.from) || + CollectionsUtil.isNil(this.selectedPeriod.to) + ) { + if ( + CollectionsUtil.isDefined(this.fromDateTime) || + CollectionsUtil.isDefined(this.toDateTime) + ) { + this.clearFilter(null); + } + } else { + const periodFromAdjusted = this._adjustDateTime( + this.selectedPeriod.from, + null, + ); + const periodToAdjusted = this._adjustDateTime( + this.selectedPeriod.to, + null, + ); + + if (!periodFromAdjusted.isBefore(periodToAdjusted)) { + return; + } + + if ( + CollectionsUtil.isDefined(this.fromDateTimeMin) && + CollectionsUtil.isDefined(this.toDateTimeMax) + ) { + if ( + periodFromAdjusted.isSameOrAfter(this.fromDateTimeMin) && + periodFromAdjusted.isBefore(this.toDateTimeMax) + ) { + this.fromDateTime = new Date( + this._adjustDateTime( + this.selectedPeriod.from, + "from", + "timezone", + ).valueOf(), + ); } - if (!CollectionsUtil.isString(this.selectedPeriodSerialized) || this.selectedPeriodSerialized.length === 0) { - if (CollectionsUtil.isDefined(this.fromDateTime) || CollectionsUtil.isDefined(this.toDateTime)) { - this.clearFilter(null); - } - } else { - const deserializedPeriodValues: DateTimePeriod = this._deserializeDateTimePeriodPairValues(this.selectedPeriodSerialized); - if (deserializedPeriodValues) { - this.fromDateTime = deserializedPeriodValues.from; - this.toDateTime = deserializedPeriodValues.to; - - this._updateForm('both'); - - this.applyFilter(null); - } + if ( + periodToAdjusted.isAfter(this.fromDateTimeMin) && + periodToAdjusted.isSameOrBefore(this.toDateTimeMax) + ) { + this.toDateTime = new Date( + this._adjustDateTime( + this.selectedPeriod.to, + "to", + "timezone", + ).valueOf(), + ); } + } else { + this.fromDateTime = new Date( + this._adjustDateTime( + this.selectedPeriod.from, + "from", + "timezone", + ).valueOf(), + ); + this.toDateTime = new Date( + this._adjustDateTime( + this.selectedPeriod.to, + "to", + "timezone", + ).valueOf(), + ); + } + + this._updateForm("both"); + + this.applyFilter(null); } - - private _changeDetectionSelectedPeriod(): void { - if (CollectionsUtil.isNil(this.selectedPeriod)) { - return; + } + + private _changeDetectionMinDateTime(): void { + // set once during initialization or forced when user clear the form + if (!this._initiallySetMin) { + this._initiallySetMin = true; + + if ( + CollectionsUtil.isDate(this.fromDateTime) && + CollectionsUtil.isDate(this.minDateTime) + ) { + if (this.fromDateTime.getTime() < this.minDateTime.getTime()) { + this.fromDateTime = this.minDateTime; } + } else { + this.fromDateTime = this.minDateTime; + } + + this.fromDateTimeMin = this._adjustDateTime( + this.minDateTime, + "min", + "utc", + ); + this.fromPickerConfig = { + ...this.fromPickerConfig, + min: this.fromDateTimeMin, + }; + + this.toDateTimeMin = this._adjustDateTime( + this.fromDateTime ?? this.minDateTime, + "min", + "utc", + ); + this.toPickerConfig = { + ...this.toPickerConfig, + min: this.toDateTimeMin, + }; + + if (!this._isFromPickerOpened) { + this._updateForm("fromDateTime"); + } + } + } - if (this.fromDateTime === this.selectedPeriod.from && this.toDateTime === this.selectedPeriod.to) { - return; - } + private _changeDetectionMaxDateTime() { + const date = new Date(); - if (CollectionsUtil.isNil(this.selectedPeriod.from) || CollectionsUtil.isNil(this.selectedPeriod.to)) { - if (CollectionsUtil.isDefined(this.fromDateTime) || CollectionsUtil.isDefined(this.toDateTime)) { - this.clearFilter(null); - } - } else { - const periodFromAdjusted = this._adjustDateTime(this.selectedPeriod.from, null); - const periodToAdjusted = this._adjustDateTime(this.selectedPeriod.to, null); - - if (!periodFromAdjusted.isBefore(periodToAdjusted)) { - return; - } - - if (CollectionsUtil.isDefined(this.fromDateTimeMin) && CollectionsUtil.isDefined(this.toDateTimeMax)) { - if (periodFromAdjusted.isSameOrAfter(this.fromDateTimeMin) && periodFromAdjusted.isBefore(this.toDateTimeMax)) { - this.fromDateTime = new Date(this._adjustDateTime(this.selectedPeriod.from, 'from', 'timezone').valueOf()); - } - - if (periodToAdjusted.isAfter(this.fromDateTimeMin) && periodToAdjusted.isSameOrBefore(this.toDateTimeMax)) { - this.toDateTime = new Date(this._adjustDateTime(this.selectedPeriod.to, 'to', 'timezone').valueOf()); - } - } else { - this.fromDateTime = new Date(this._adjustDateTime(this.selectedPeriod.from, 'from', 'timezone').valueOf()); - this.toDateTime = new Date(this._adjustDateTime(this.selectedPeriod.to, 'to', 'timezone').valueOf()); - } - - this._updateForm('both'); - - this.applyFilter(null); - } - } + this.toDateTimeMax = this._adjustDateTime(date, "max", "utc"); + this.toPickerConfig = { + ...this.toPickerConfig, + max: this.toDateTimeMax, + }; - private _changeDetectionMinDateTime(): void { - // set once during initialization or forced when user clear the form - if (!this._initiallySetMin) { - this._initiallySetMin = true; - - if (CollectionsUtil.isDate(this.fromDateTime) && CollectionsUtil.isDate(this.minDateTime)) { - if (this.fromDateTime.getTime() < this.minDateTime.getTime()) { - this.fromDateTime = this.minDateTime; - } - } else { - this.fromDateTime = this.minDateTime; - } - - this.fromDateTimeMin = this._adjustDateTime(this.minDateTime, 'min', 'utc'); - this.fromPickerConfig = { - ...this.fromPickerConfig, - min: this.fromDateTimeMin - }; - - this.toDateTimeMin = this._adjustDateTime(this.fromDateTime ?? this.minDateTime, 'min', 'utc'); - this.toPickerConfig = { - ...this.toPickerConfig, - min: this.toDateTimeMin - }; - - if (!this._isFromPickerOpened) { - this._updateForm('fromDateTime'); - } - } - } + // set once during initialization or forced when user clear the form + if (!this._initiallySetMax) { + this._initiallySetMax = true; - private _changeDetectionMaxDateTime() { - const date = new Date(); - - this.toDateTimeMax = this._adjustDateTime(date, 'max', 'utc'); - this.toPickerConfig = { - ...this.toPickerConfig, - max: this.toDateTimeMax - }; - - // set once during initialization or forced when user clear the form - if (!this._initiallySetMax) { - this._initiallySetMax = true; - - if (CollectionsUtil.isDate(this.toDateTime)) { - if (this.toDateTime.getTime() > date.getTime()) { - this.toDateTime = date; - } - } else { - this.toDateTime = date; - } - - this.fromDateTimeMax = this._adjustDateTime(this.toDateTime, 'max', 'utc'); - this.fromPickerConfig = { - ...this.fromPickerConfig, - max: this.fromDateTimeMax - }; - - if (!this._isToPickerOpened) { - this._updateForm('toDateTime'); - } + if (CollectionsUtil.isDate(this.toDateTime)) { + if (this.toDateTime.getTime() > date.getTime()) { + this.toDateTime = date; } + } else { + this.toDateTime = date; + } + + this.fromDateTimeMax = this._adjustDateTime( + this.toDateTime, + "max", + "utc", + ); + this.fromPickerConfig = { + ...this.fromPickerConfig, + max: this.fromDateTimeMax, + }; + + if (!this._isToPickerOpened) { + this._updateForm("toDateTime"); + } } + } - private _serializeDateTimePeriodPairValues(): string { - let timePeriodFilter = ''; - - if (this.fromDateTime instanceof Date) { - timePeriodFilter += `${this.fromDateTime.getTime()}`; - } + private _serializeDateTimePeriodPairValues(): string { + let timePeriodFilter = ""; - if (this.toDateTime instanceof Date) { - timePeriodFilter += `-${this.toDateTime.getTime()}`; - } + if (this.fromDateTime instanceof Date) { + timePeriodFilter += `${this.fromDateTime.getTime()}`; + } - return timePeriodFilter; + if (this.toDateTime instanceof Date) { + timePeriodFilter += `-${this.toDateTime.getTime()}`; } - private _deserializeDateTimePeriodPairValues(dateTimePeriodValues: string): DateTimePeriod { - const fromToDateTimeTuple = dateTimePeriodValues.split('-'); + return timePeriodFilter; + } + + private _deserializeDateTimePeriodPairValues( + dateTimePeriodValues: string, + ): DateTimePeriod { + const fromToDateTimeTuple = dateTimePeriodValues.split("-"); - let fromDateTime: Date; - let toDateTime: Date; + let fromDateTime: Date; + let toDateTime: Date; - if (CollectionsUtil.isStringWithContent(fromToDateTimeTuple[0]) && /\d+/.test(fromToDateTimeTuple[0])) { - const parsedFromEpochTime = parseInt(fromToDateTimeTuple[0], 10); + if ( + CollectionsUtil.isStringWithContent(fromToDateTimeTuple[0]) && + /\d+/.test(fromToDateTimeTuple[0]) + ) { + const parsedFromEpochTime = parseInt(fromToDateTimeTuple[0], 10); - if (CollectionsUtil.isNumber(parsedFromEpochTime) && !CollectionsUtil.isNaN(parsedFromEpochTime)) { - fromDateTime = new Date(parsedFromEpochTime); + if ( + CollectionsUtil.isNumber(parsedFromEpochTime) && + !CollectionsUtil.isNaN(parsedFromEpochTime) + ) { + fromDateTime = new Date(parsedFromEpochTime); - if (CollectionsUtil.isNaN(fromDateTime.valueOf())) { - fromDateTime = null; - } - } + if (CollectionsUtil.isNaN(fromDateTime.valueOf())) { + fromDateTime = null; } + } + } - if (CollectionsUtil.isStringWithContent(fromToDateTimeTuple[1]) && /\d+/.test(fromToDateTimeTuple[1])) { - const parsedToEpochTime = parseInt(fromToDateTimeTuple[1], 10); + if ( + CollectionsUtil.isStringWithContent(fromToDateTimeTuple[1]) && + /\d+/.test(fromToDateTimeTuple[1]) + ) { + const parsedToEpochTime = parseInt(fromToDateTimeTuple[1], 10); - if (CollectionsUtil.isNumber(parsedToEpochTime) && !CollectionsUtil.isNaN(parsedToEpochTime)) { - toDateTime = new Date(parsedToEpochTime); + if ( + CollectionsUtil.isNumber(parsedToEpochTime) && + !CollectionsUtil.isNaN(parsedToEpochTime) + ) { + toDateTime = new Date(parsedToEpochTime); - if (CollectionsUtil.isNaN(toDateTime.valueOf())) { - toDateTime = null; - } - } + if (CollectionsUtil.isNaN(toDateTime.valueOf())) { + toDateTime = null; } + } + } - if (CollectionsUtil.isDate(fromDateTime) && CollectionsUtil.isDate(toDateTime)) { - const fromDateTimeMoment = this._adjustDateTime(fromDateTime, null, 'utc'); - const toDateTimeMoment = this._adjustDateTime(toDateTime, null, 'utc'); - - if (fromDateTimeMoment.isSameOrAfter(toDateTimeMoment)) { - return null; - } - - if (CollectionsUtil.isDefined(this.fromDateTimeMin) && CollectionsUtil.isDefined(this.fromDateTimeMax)) { - if (!fromDateTimeMoment.isBetween(this.fromDateTimeMin, this.fromDateTimeMax)) { - return null; - } - } - - if (CollectionsUtil.isDefined(this.toDateTimeMin) && CollectionsUtil.isDefined(this.toDateTimeMax)) { - if (!toDateTimeMoment.isBetween(this.toDateTimeMin, this.toDateTimeMax)) { - return null; - } - } - - return { - from: fromDateTime, - to: toDateTime - }; + if ( + CollectionsUtil.isDate(fromDateTime) && + CollectionsUtil.isDate(toDateTime) + ) { + const fromDateTimeMoment = this._adjustDateTime( + fromDateTime, + null, + "utc", + ); + const toDateTimeMoment = this._adjustDateTime(toDateTime, null, "utc"); + + if (fromDateTimeMoment.isSameOrAfter(toDateTimeMoment)) { + return null; + } + + if ( + CollectionsUtil.isDefined(this.fromDateTimeMin) && + CollectionsUtil.isDefined(this.fromDateTimeMax) + ) { + if ( + !fromDateTimeMoment.isBetween( + this.fromDateTimeMin, + this.fromDateTimeMax, + ) + ) { + return null; + } + } + + if ( + CollectionsUtil.isDefined(this.toDateTimeMin) && + CollectionsUtil.isDefined(this.toDateTimeMax) + ) { + if ( + !toDateTimeMoment.isBetween(this.toDateTimeMin, this.toDateTimeMax) + ) { + return null; } + } - return null; + return { + from: fromDateTime, + to: toDateTime, + }; } - private _normalizeUrlAfterLoad(): void { - if (this._isUrlNormalized) { - return; - } + return null; + } - if (!this.filtersSortManager) { - return; - } + private _normalizeUrlAfterLoad(): void { + if (this._isUrlNormalized) { + return; + } - if (!this.filtersSortManager.hasFilter(FILTER_TIME_PERIOD_KEY)) { - return; - } + if (!this.filtersSortManager) { + return; + } - this._isUrlNormalized = true; + if (!this.filtersSortManager.hasFilter(FILTER_TIME_PERIOD_KEY)) { + return; + } - if (this.filtersSortManager.filterCriteria[FILTER_TIME_PERIOD_KEY] === this._serializeDateTimePeriodPairValues()) { - return; - } + this._isUrlNormalized = true; - this.filtersSortManager.setFilter(FILTER_TIME_PERIOD_KEY, this._serializeDateTimePeriodPairValues()); - this.filtersSortManager.updateBrowserUrl('replaceToURL', true); + if ( + this.filtersSortManager.filterCriteria[FILTER_TIME_PERIOD_KEY] === + this._serializeDateTimePeriodPairValues() + ) { + return; } - private _updateForm(partial: 'fromDateTime' | 'toDateTime' | 'both'): void { - if (partial === 'both' || partial === 'fromDateTime') { - this.tmForm - .get('fromDateTime') - .patchValue(this._adjustDateTime(this.fromDateTime, 'from', 'utc').format(this.fromPickerConfig.format)); - } - - if (partial === 'both' || partial === 'toDateTime') { - this.tmForm.get('toDateTime').patchValue(this._adjustDateTime(this.toDateTime, 'to', 'utc').format(this.toPickerConfig.format)); - } + this.filtersSortManager.setFilter( + FILTER_TIME_PERIOD_KEY, + this._serializeDateTimePeriodPairValues(), + ); + this.filtersSortManager.updateBrowserUrl("replaceToURL", true); + } + + private _updateForm(partial: "fromDateTime" | "toDateTime" | "both"): void { + if (partial === "both" || partial === "fromDateTime") { + this.tmForm + .get("fromDateTime") + .patchValue( + this._adjustDateTime(this.fromDateTime, "from", "utc").format( + this.fromPickerConfig.format, + ), + ); } - private _emitChanges(): void { - this.filterChanged.emit({ - from: this.fromDateTime, - to: this.toDateTime - }); + if (partial === "both" || partial === "toDateTime") { + this.tmForm + .get("toDateTime") + .patchValue( + this._adjustDateTime(this.toDateTime, "to", "utc").format( + this.toPickerConfig.format, + ), + ); } + } + + private _emitChanges(): void { + this.filterChanged.emit({ + from: this.fromDateTime, + to: this.toDateTime, + }); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/public-api.ts index ff32bd3607..64ac2276b7 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/public-api.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-page.component'; -export * from './pages/details/public-api'; -export * from './pages/executions/public-api'; +export * from "./data-job-page.component"; +export * from "./pages/details/public-api"; +export * from "./pages/executions/public-api"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.html index 0e85771b46..d72159e84e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.html @@ -6,508 +6,482 @@
- -
- -
-
- -
+
+ +
+ + +
+ - Job name + + + Team name + + + + - Job name - - - Team name - - - - - Description - - - - - - Deployment Status - - - - - - - - - Python Version - - - - - Last Execution End (UTC) - - - - Last Execution Duration - - - - Last Execution Status - - - - - - - - - - - - Success rate - - - - - Calculating up to 336 executions from last 14 days - - - - - Schedule (UTC) - - - Next run (UTC) - - - - - Last Deployed (UTC) - - - - Last Deployed By - - - - - Source - - - - - Logs - - - - View - - - - - - {{ job.jobName }} - - - {{ job.jobName }} - - - - - {{ job.config.team }} - - - - {{ job.config?.description | words : 8 }} - - - - - - - {{ job.deployments ? job.deployments[0]?.jobPythonVersion : - null }} - - - - {{ job.deployments ? (job.deployments[0]?.lastExecutionTime - | date : "MMM d, y, hh:mm a" : "utc") : null }} - - - - {{ job.deployments ? - (job.deployments[0]?.lastExecutionDuration | formatDuration) - : null }} - - - - - - - {{ job.deployments | executionSuccessRate }} - - {{ job.config?.schedule?.scheduleCron | formatSchedule : "" - }} - - - {{ job.config?.schedule?.nextRunEpochSeconds | - parseEpoch | date : "MMM d, y, hh:mm a" : "UTC" }} - - - - - {{ job.deployments && job.deployments[0]?.lastDeployedDate ? - (job.deployments[0]?.lastDeployedDate | date : "MMM d, y, - hh:mm a" : "UTC") : null }} - - - - {{ job.deployments ? job.deployments[0]?.lastDeployedBy : - null }} - - - -
-
-
- - - -
-
-
- - - - - - - - - - - - -
- - - - -
- No data jobs created! -
-
- No data jobs that match with - {{ clrGridUIState.search }} - criteria -
-
-
-
- - - - Data Jobs per page - {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 - }} of {{ pagination.totalItems }} Data Jobs - - -
-
+ Description + + + + + + Deployment Status + + + + + + + + + Python Version + + + + + Last Execution End (UTC) + + + + Last Execution Duration + + + + Last Execution Status + + + + + + + + + + + + Success rate + + + + + Calculating up to 336 executions from last 14 days + + + + + Schedule (UTC) + + + Next run (UTC) + + + + + Last Deployed (UTC) + + + + Last Deployed By + + + + + Source + + + + + Logs + + + + View + + + + + + {{ job.jobName }} + + + {{ job.jobName }} + + + + + {{ + job.config.team + }} + + + + {{ job.config?.description | words: 8 }} + + + + + + + {{ job.deployments ? job.deployments[0]?.jobPythonVersion : null }} + + + + {{ + job.deployments + ? (job.deployments[0]?.lastExecutionTime + | date: "MMM d, y, hh:mm a" : "utc") + : null + }} + + + + {{ + job.deployments + ? (job.deployments[0]?.lastExecutionDuration | formatDuration) + : null + }} + + + + + + + {{ job.deployments | executionSuccessRate }} + + {{ + job.config?.schedule?.scheduleCron | formatSchedule: "" + }} + + + {{ + job.config?.schedule?.nextRunEpochSeconds + | parseEpoch + | date: "MMM d, y, hh:mm a" : "UTC" + }} + + + + + {{ + job.deployments && job.deployments[0]?.lastDeployedDate + ? (job.deployments[0]?.lastDeployedDate + | date + : "MMM d, y, + hh:mm a" + : "UTC") + : null + }} + + + + {{ job.deployments ? job.deployments[0]?.lastDeployedBy : null }} + + + +
+
+
+ + + +
+
+
+ + + + + + + + + + + + +
+ + + + +
No data jobs created!
+
+ No data jobs that match with + {{ clrGridUIState.search }} + criteria +
+
+
+
+ + + + Data Jobs per page + {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of + {{ pagination.totalItems }} Data Jobs + + + +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.scss index 8c72a072ec..42ebd80f19 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.scss @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import '../../../base-grid/data-jobs-base-grid.component'; +@import "../../../base-grid/data-jobs-base-grid.component"; .label-link-suppress-decoration { - &:hover { - text-decoration: none; - } + &:hover { + text-decoration: none; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.spec.ts index e543e8f685..2c5e8e851a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.spec.ts @@ -3,356 +3,466 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { Location } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { Location } from "@angular/common"; +import { ActivatedRoute, Router } from "@angular/router"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; +import { BehaviorSubject, Observable, of, Subject } from "rxjs"; -import { ClrDatagridStateInterface } from '@clr/angular'; +import { ClrDatagridStateInterface } from "@clr/angular"; -import { ApolloQueryResult } from '@apollo/client/core'; +import { ApolloQueryResult } from "@apollo/client/core"; import { - ASC, - CallFake, - ComponentModel, - ComponentService, - ComponentStateImpl, - ErrorHandlerService, - generateErrorCodes, - NavigationService, - RouterService, - RouterState, - SystemEventDispatcher, - URLStateManager -} from '@versatiledatakit/shared'; - -import { DATA_PIPELINES_CONFIGS, DataJobDetails, DataJobPage, DisplayMode } from '../../../../model'; - -import { DataJobsApiService, DataJobsService } from '../../../../services'; - -import { TASK_LOAD_JOBS_STATE } from '../../../../state/tasks'; -import { LOAD_JOBS_ERROR_CODES } from '../../../../state/error-codes'; - -import { QUERY_PARAM_SEARCH } from '../../../base-grid/data-jobs-base-grid.component'; - -import { DataJobsExploreGridComponent } from './data-jobs-explore-grid.component'; - -describe('DataJobsExploreGridComponent', () => { - let componentServiceStub: jasmine.SpyObj; - let dataJobsApiServiceStub: jasmine.SpyObj; - let locationStub: jasmine.SpyObj; - let navigationServiceStub: jasmine.SpyObj; - let routerServiceStub: jasmine.SpyObj; - let dataJobsServiceStub: jasmine.SpyObj; - let errorHandlerServiceStub: jasmine.SpyObj; - - let componentModelStub: ComponentModel; - let component: DataJobsExploreGridComponent; - let fixture: ComponentFixture; - - const TEST_JOB = { - jobName: 'job001' - }; - - beforeAll(() => { - // This hack is needed in order to prevent tests to reload browser. - // Browser reloading stops Tests execution in the same browser, and need to restart execution from beginning. - window.onbeforeunload = () => 'Stop browser from reload!'; + ASC, + CallFake, + ComponentModel, + ComponentService, + ComponentStateImpl, + ErrorHandlerService, + generateErrorCodes, + NavigationService, + RouterService, + RouterState, + SystemEventDispatcher, + URLStateManager, +} from "@versatiledatakit/shared"; + +import { + DATA_PIPELINES_CONFIGS, + DataJobDetails, + DataJobPage, + DisplayMode, +} from "../../../../model"; + +import { DataJobsApiService, DataJobsService } from "../../../../services"; + +import { TASK_LOAD_JOBS_STATE } from "../../../../state/tasks"; +import { LOAD_JOBS_ERROR_CODES } from "../../../../state/error-codes"; + +import { QUERY_PARAM_SEARCH } from "../../../base-grid/data-jobs-base-grid.component"; + +import { DataJobsExploreGridComponent } from "./data-jobs-explore-grid.component"; + +describe("DataJobsExploreGridComponent", () => { + let componentServiceStub: jasmine.SpyObj; + let dataJobsApiServiceStub: jasmine.SpyObj; + let locationStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let routerServiceStub: jasmine.SpyObj; + let dataJobsServiceStub: jasmine.SpyObj; + let errorHandlerServiceStub: jasmine.SpyObj; + + let componentModelStub: ComponentModel; + let component: DataJobsExploreGridComponent; + let fixture: ComponentFixture; + + const TEST_JOB = { + jobName: "job001", + }; + + beforeAll(() => { + // This hack is needed in order to prevent tests to reload browser. + // Browser reloading stops Tests execution in the same browser, and need to restart execution from beginning. + window.onbeforeunload = () => "Stop browser from reload!"; + }); + + beforeEach(() => { + const activatedRouteStub = () => ({ + queryParams: { + subscribe: CallFake, + }, + snapshot: null, + }); + const routerStub = () => ({ + url: "/explore/data-jobs", }); - beforeEach(() => { - const activatedRouteStub = () => ({ - queryParams: { - subscribe: CallFake - }, - snapshot: null - }); - const routerStub = () => ({ - url: '/explore/data-jobs' - }); - - componentServiceStub = jasmine.createSpyObj('componentService', ['init', 'getModel', 'idle', 'update']); - navigationServiceStub = jasmine.createSpyObj('navigationService', ['navigate', 'navigateTo', 'navigateBack']); - dataJobsApiServiceStub = jasmine.createSpyObj('dataJobsService', [ - 'getJobs', - 'getJobDetails', - 'getJobExecutions' - ]); - dataJobsServiceStub = jasmine.createSpyObj('dataJobsService', [ - 'loadJobs', - 'notifyForRunningJobExecutionId', - 'notifyForJobExecutions', - 'notifyForTeamImplicitly', - 'getNotifiedForRunningJobExecutionId', - 'getNotifiedForJobExecutions', - 'getNotifiedForTeamImplicitly' - ]); - locationStub = jasmine.createSpyObj('location', ['path', 'go']); - routerServiceStub = jasmine.createSpyObj('routerService', ['getState', 'get']); - errorHandlerServiceStub = jasmine.createSpyObj('errorHandlerService', ['processError', 'handleError']); - - dataJobsServiceStub.getNotifiedForJobExecutions.and.returnValue(new Subject()); - dataJobsServiceStub.getNotifiedForTeamImplicitly.and.returnValue(new BehaviorSubject('taurus')); - - componentModelStub = ComponentModel.of(ComponentStateImpl.of({}), RouterState.empty()); - routerServiceStub.getState.and.returnValue(new Subject()); - routerServiceStub.get.and.returnValue(new Subject()); - componentServiceStub.init.and.returnValue(of(componentModelStub)); - componentServiceStub.getModel.and.returnValue(of(componentModelStub)); - - generateErrorCodes(dataJobsApiServiceStub, ['getJobs', 'getJobDetails', 'getJobExecutions']); - - LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE] = dataJobsApiServiceStub.errorCodes.getJobs; - - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DataJobsExploreGridComponent], - providers: [ - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => ({ - defaultOwnerTeamName: 'all' - }) - }, - { provide: RouterService, useValue: routerServiceStub }, - { provide: ComponentService, useValue: componentServiceStub }, - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: ActivatedRoute, useFactory: activatedRouteStub }, - { - provide: DataJobsApiService, - useValue: dataJobsApiServiceStub - }, - { provide: DataJobsService, useValue: dataJobsServiceStub }, - { provide: Location, useValue: locationStub }, - { provide: Router, useFactory: routerStub }, - { - provide: ErrorHandlerService, - useValue: errorHandlerServiceStub - } - ] - }); - - fixture = TestBed.createComponent(DataJobsExploreGridComponent); - component = fixture.componentInstance; - component.teamNameFilter = 'testFilterTeam'; - component.model = componentModelStub; - - dataJobsApiServiceStub.getJobs.and.returnValue( - of({ - data: { - content: [], - totalItems: 0 - } - } as ApolloQueryResult) - ); - dataJobsApiServiceStub.getJobDetails.and.returnValue(of(null) as Observable); - dataJobsApiServiceStub.getJobExecutions.and.returnValue(of({ content: [], totalItems: 0, totalPages: 0 })); - - locationStub.path.and.returnValue('/explore/data-jobs'); - - spyOn(SystemEventDispatcher, 'send').and.returnValue(Promise.resolve(true)); + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["init", "getModel", "idle", "update"], + ); + navigationServiceStub = jasmine.createSpyObj( + "navigationService", + ["navigate", "navigateTo", "navigateBack"], + ); + dataJobsApiServiceStub = jasmine.createSpyObj( + "dataJobsService", + ["getJobs", "getJobDetails", "getJobExecutions"], + ); + dataJobsServiceStub = jasmine.createSpyObj( + "dataJobsService", + [ + "loadJobs", + "notifyForRunningJobExecutionId", + "notifyForJobExecutions", + "notifyForTeamImplicitly", + "getNotifiedForRunningJobExecutionId", + "getNotifiedForJobExecutions", + "getNotifiedForTeamImplicitly", + ], + ); + locationStub = jasmine.createSpyObj("location", ["path", "go"]); + routerServiceStub = jasmine.createSpyObj("routerService", [ + "getState", + "get", + ]); + errorHandlerServiceStub = jasmine.createSpyObj( + "errorHandlerService", + ["processError", "handleError"], + ); + + dataJobsServiceStub.getNotifiedForJobExecutions.and.returnValue( + new Subject(), + ); + dataJobsServiceStub.getNotifiedForTeamImplicitly.and.returnValue( + new BehaviorSubject("taurus"), + ); + + componentModelStub = ComponentModel.of( + ComponentStateImpl.of({}), + RouterState.empty(), + ); + routerServiceStub.getState.and.returnValue(new Subject()); + routerServiceStub.get.and.returnValue(new Subject()); + componentServiceStub.init.and.returnValue(of(componentModelStub)); + componentServiceStub.getModel.and.returnValue(of(componentModelStub)); + + generateErrorCodes(dataJobsApiServiceStub, [ + "getJobs", + "getJobDetails", + "getJobExecutions", + ]); + + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE] = + dataJobsApiServiceStub.errorCodes.getJobs; + + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DataJobsExploreGridComponent], + providers: [ + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => ({ + defaultOwnerTeamName: "all", + }), + }, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ComponentService, useValue: componentServiceStub }, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: ActivatedRoute, useFactory: activatedRouteStub }, + { + provide: DataJobsApiService, + useValue: dataJobsApiServiceStub, + }, + { provide: DataJobsService, useValue: dataJobsServiceStub }, + { provide: Location, useValue: locationStub }, + { provide: Router, useFactory: routerStub }, + { + provide: ErrorHandlerService, + useValue: errorHandlerServiceStub, + }, + ], }); - it('can load instance', () => { - expect(component).toBeTruthy(); + fixture = TestBed.createComponent(DataJobsExploreGridComponent); + component = fixture.componentInstance; + component.teamNameFilter = "testFilterTeam"; + component.model = componentModelStub; + + dataJobsApiServiceStub.getJobs.and.returnValue( + of({ + data: { + content: [], + totalItems: 0, + }, + } as ApolloQueryResult), + ); + dataJobsApiServiceStub.getJobDetails.and.returnValue( + of(null) as Observable, + ); + dataJobsApiServiceStub.getJobExecutions.and.returnValue( + of({ content: [], totalItems: 0, totalPages: 0 }), + ); + + locationStub.path.and.returnValue("/explore/data-jobs"); + + spyOn(SystemEventDispatcher, "send").and.returnValue(Promise.resolve(true)); + }); + + it("can load instance", () => { + expect(component).toBeTruthy(); + }); + + it(`displayMode has default value`, () => { + expect(component.displayMode).toEqual(DisplayMode.STANDARD); + }); + + it(`loading has default value`, () => { + expect(component.loading).toBeFalse(); + }); + + it(`dataJobs has default value`, () => { + expect(component.dataJobs).toEqual([]); + }); + + it(`totalJobs has default value`, () => { + expect(component.totalJobs).toEqual(0); + }); + + describe("urlUpdateStrategy", () => { + it("should verify the behaviour of _doUrlUpdate when urlUpdateStrategy is default (updateRouter)", () => { + const locationToUrlSpy = spyOn( + component.urlStateManager, + "locationToURL", + ).and.callFake(CallFake); + + // @ts-ignore + component._doUrlUpdate(); + + expect(locationToUrlSpy).toHaveBeenCalled(); }); - it(`displayMode has default value`, () => { - expect(component.displayMode).toEqual(DisplayMode.STANDARD); + it("should verify the behaviour of _doUrlUpdate when urlUpdateStrategy is changed (updateLocation)", () => { + const locationToURLSpy = spyOn( + component.urlStateManager, + "locationToURL", + ).and.callFake(CallFake); + component.urlUpdateStrategy = "updateLocation"; + + // @ts-ignore + component._doUrlUpdate(); + + expect(locationToURLSpy).toHaveBeenCalled(); }); + }); - it(`loading has default value`, () => { - expect(component.loading).toBeFalse(); + describe("urlStateManager", () => { + beforeEach(() => { + fixture.detectChanges(); }); - it(`dataJobs has default value`, () => { - expect(component.dataJobs).toEqual([]); + it("should verify will invoke default urlStateManager (locally created)", () => { + // Given + const setQueryParamSpy = spyOn( + component.urlStateManager, + "setQueryParam", + ).and.callFake(CallFake); + + // When + component.search("search"); + + // Then + expect(setQueryParamSpy.calls.argsFor(0)).toEqual([ + "jobName", + undefined, + 1, + ]); + expect(setQueryParamSpy.calls.argsFor(1)).toEqual([ + "teamName", + undefined, + 2, + ]); + expect(setQueryParamSpy.calls.argsFor(2)).toEqual([ + "description", + undefined, + 3, + ]); + expect(setQueryParamSpy.calls.argsFor(3)).toEqual([ + "deploymentStatus", + "all", + 4, + ]); + expect(setQueryParamSpy.calls.argsFor(4)).toEqual([ + "deploymentLastExecutionStatus", + undefined, + 5, + ]); + expect(setQueryParamSpy.calls.argsFor(5)).toEqual([ + QUERY_PARAM_SEARCH, + "search", + 0, + ]); }); - it(`totalJobs has default value`, () => { - expect(component.totalJobs).toEqual(0); + it("should verify will invoke external urlStateManager (dependency injected)", () => { + // Given + const urlStateManagerStub = new URLStateManager("baseUrl", locationStub); + const setQueryParamSpy = spyOn( + urlStateManagerStub, + "setQueryParam", + ).and.callFake(CallFake); + + // When + component.urlStateManager = urlStateManagerStub; + component.search("search"); + + // Then + expect(setQueryParamSpy.calls.argsFor(0)).toEqual([ + "jobName", + undefined, + 1, + ]); + expect(setQueryParamSpy.calls.argsFor(1)).toEqual([ + "teamName", + undefined, + 2, + ]); + expect(setQueryParamSpy.calls.argsFor(2)).toEqual([ + "description", + undefined, + 3, + ]); + expect(setQueryParamSpy.calls.argsFor(3)).toEqual([ + "deploymentStatus", + "all", + 4, + ]); + expect(setQueryParamSpy.calls.argsFor(4)).toEqual([ + "deploymentLastExecutionStatus", + undefined, + 5, + ]); + expect(setQueryParamSpy.calls.argsFor(5)).toEqual([ + QUERY_PARAM_SEARCH, + "search", + 0, + ]); }); - describe('urlUpdateStrategy', () => { - it('should verify the behaviour of _doUrlUpdate when urlUpdateStrategy is default (updateRouter)', () => { - const locationToUrlSpy = spyOn(component.urlStateManager, 'locationToURL').and.callFake(CallFake); + it("should verify will invoke default urlStateManager (dependency injection is null or undefined)", () => { + // Given + const setQueryParamSpy = spyOn( + component.urlStateManager, + "setQueryParam", + ).and.callFake(CallFake); + // When + component.urlStateManager = null; + component.search("search test value 1"); + component.urlStateManager = undefined; + component.search("search test value 2"); + + // Then + expect(setQueryParamSpy.calls.argsFor(5)).toEqual([ + QUERY_PARAM_SEARCH, + "search test value 1", + 0, + ]); + expect(setQueryParamSpy.calls.argsFor(11)).toEqual([ + QUERY_PARAM_SEARCH, + "search test value 2", + 0, + ]); + }); + }); - // @ts-ignore - component._doUrlUpdate(); + describe("handleStateChange", () => { + beforeEach(() => { + fixture.detectChanges(); + }); - expect(locationToUrlSpy).toHaveBeenCalled(); - }); + it("makes expected calls", () => { + const clrDatagridStateInterfaceStub = { + filters: [], + } as ClrDatagridStateInterface; + clrDatagridStateInterfaceStub.filters.push({ + property: "search_prop", + pattern: "%search%", + sort: ASC, + }); + component.gridState = clrDatagridStateInterfaceStub; + + // @ts-ignore + spyOn(component, "_createApiFilterPattern").and.callThrough(); + // @ts-ignore + component._doLoadData(); + // @ts-ignore + expect(component._createApiFilterPattern).toHaveBeenCalled(); + expect(dataJobsServiceStub.loadJobs).toHaveBeenCalled(); + }); + }); - it('should verify the behaviour of _doUrlUpdate when urlUpdateStrategy is changed (updateLocation)', () => { - const locationToURLSpy = spyOn(component.urlStateManager, 'locationToURL').and.callFake(CallFake); - component.urlUpdateStrategy = 'updateLocation'; + describe("viewJobDetails", () => { + it("skips viewJobDetails for undefined job", () => { + // Given + const navigateToSpy = spyOn(component, "navigateTo").and.callFake( + CallFake, + ); - // @ts-ignore - component._doUrlUpdate(); + // When + component.navigateToJobDetails(); - expect(locationToURLSpy).toHaveBeenCalled(); - }); + // Then + expect(component.selectedJob).toBeUndefined(); + expect(navigateToSpy).not.toHaveBeenCalled(); }); - describe('urlStateManager', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - it('should verify will invoke default urlStateManager (locally created)', () => { - // Given - const setQueryParamSpy = spyOn(component.urlStateManager, 'setQueryParam').and.callFake(CallFake); - - // When - component.search('search'); - - // Then - expect(setQueryParamSpy.calls.argsFor(0)).toEqual(['jobName', undefined, 1]); - expect(setQueryParamSpy.calls.argsFor(1)).toEqual(['teamName', undefined, 2]); - expect(setQueryParamSpy.calls.argsFor(2)).toEqual(['description', undefined, 3]); - expect(setQueryParamSpy.calls.argsFor(3)).toEqual(['deploymentStatus', 'all', 4]); - expect(setQueryParamSpy.calls.argsFor(4)).toEqual(['deploymentLastExecutionStatus', undefined, 5]); - expect(setQueryParamSpy.calls.argsFor(5)).toEqual([QUERY_PARAM_SEARCH, 'search', 0]); - }); - - it('should verify will invoke external urlStateManager (dependency injected)', () => { - // Given - const urlStateManagerStub = new URLStateManager('baseUrl', locationStub); - const setQueryParamSpy = spyOn(urlStateManagerStub, 'setQueryParam').and.callFake(CallFake); - - // When - component.urlStateManager = urlStateManagerStub; - component.search('search'); - - // Then - expect(setQueryParamSpy.calls.argsFor(0)).toEqual(['jobName', undefined, 1]); - expect(setQueryParamSpy.calls.argsFor(1)).toEqual(['teamName', undefined, 2]); - expect(setQueryParamSpy.calls.argsFor(2)).toEqual(['description', undefined, 3]); - expect(setQueryParamSpy.calls.argsFor(3)).toEqual(['deploymentStatus', 'all', 4]); - expect(setQueryParamSpy.calls.argsFor(4)).toEqual(['deploymentLastExecutionStatus', undefined, 5]); - expect(setQueryParamSpy.calls.argsFor(5)).toEqual([QUERY_PARAM_SEARCH, 'search', 0]); - }); - - it('should verify will invoke default urlStateManager (dependency injection is null or undefined)', () => { - // Given - const setQueryParamSpy = spyOn(component.urlStateManager, 'setQueryParam').and.callFake(CallFake); - // When - component.urlStateManager = null; - component.search('search test value 1'); - component.urlStateManager = undefined; - component.search('search test value 2'); - - // Then - expect(setQueryParamSpy.calls.argsFor(5)).toEqual([QUERY_PARAM_SEARCH, 'search test value 1', 0]); - expect(setQueryParamSpy.calls.argsFor(11)).toEqual([QUERY_PARAM_SEARCH, 'search test value 2', 0]); - }); + it("opens viewJobDetails for valid job", () => { + // Given + const navigateToSpy = spyOn(component, "navigateTo").and.returnValue( + Promise.resolve(true), + ); + component.ngOnInit(); + + // When + component.navigateToJobDetails({ + jobName: "job001", + config: { team: "team007" }, + }); + + // Then + expect(component.selectedJob).toBeDefined(); + expect(navigateToSpy).toHaveBeenCalledWith({ + "$.team": "team007", + "$.job": "job001", + }); + }); + }); + + describe("ngOnInit", () => { + it("makes expected calls", () => { + // @ts-ignore + component.ngOnInit(); + expect(componentServiceStub.init).toHaveBeenCalled(); + expect(componentServiceStub.getModel).toHaveBeenCalled(); }); + }); - describe('handleStateChange', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - it('makes expected calls', () => { - const clrDatagridStateInterfaceStub = { - filters: [] - } as ClrDatagridStateInterface; - clrDatagridStateInterfaceStub.filters.push({ - property: 'search_prop', - pattern: '%search%', - sort: ASC - }); - component.gridState = clrDatagridStateInterfaceStub; - - // @ts-ignore - spyOn(component, '_createApiFilterPattern').and.callThrough(); - // @ts-ignore - component._doLoadData(); - // @ts-ignore - expect(component._createApiFilterPattern).toHaveBeenCalled(); - expect(dataJobsServiceStub.loadJobs).toHaveBeenCalled(); - }); + describe("isStandardDisplayMode", () => { + it("returns expected displayMode with COMPACT", () => { + component.displayMode = DisplayMode.COMPACT; + expect(component.isStandardDisplayMode()).toBeFalse(); }); - describe('viewJobDetails', () => { - it('skips viewJobDetails for undefined job', () => { - // Given - const navigateToSpy = spyOn(component, 'navigateTo').and.callFake(CallFake); - - // When - component.navigateToJobDetails(); - - // Then - expect(component.selectedJob).toBeUndefined(); - expect(navigateToSpy).not.toHaveBeenCalled(); - }); - - it('opens viewJobDetails for valid job', () => { - // Given - const navigateToSpy = spyOn(component, 'navigateTo').and.returnValue(Promise.resolve(true)); - component.ngOnInit(); - - // When - component.navigateToJobDetails({ - jobName: 'job001', - config: { team: 'team007' } - }); - - // Then - expect(component.selectedJob).toBeDefined(); - expect(navigateToSpy).toHaveBeenCalledWith({ - '$.team': 'team007', - '$.job': 'job001' - }); - }); + it("returns expected displayMode with null", () => { + component.displayMode = null; + expect(component.isStandardDisplayMode()).toBeFalse(); }); - describe('ngOnInit', () => { - it('makes expected calls', () => { - // @ts-ignore - component.ngOnInit(); - expect(componentServiceStub.init).toHaveBeenCalled(); - expect(componentServiceStub.getModel).toHaveBeenCalled(); - }); + it("returns expected displayMode with STANDARD", () => { + component.displayMode = DisplayMode.STANDARD; + expect(component.isStandardDisplayMode()).toBeTrue(); }); + }); - describe('isStandardDisplayMode', () => { - it('returns expected displayMode with COMPACT', () => { - component.displayMode = DisplayMode.COMPACT; - expect(component.isStandardDisplayMode()).toBeFalse(); - }); - - it('returns expected displayMode with null', () => { - component.displayMode = null; - expect(component.isStandardDisplayMode()).toBeFalse(); - }); - - it('returns expected displayMode with STANDARD', () => { - component.displayMode = DisplayMode.STANDARD; - expect(component.isStandardDisplayMode()).toBeTrue(); - }); + describe("selectionChanged", () => { + it("sets expected dataJob", () => { + component.selectionChanged(TEST_JOB); + expect(component.selectedJob).toEqual(TEST_JOB); }); + }); - describe('selectionChanged', () => { - it('sets expected dataJob', () => { - component.selectionChanged(TEST_JOB); - expect(component.selectedJob).toEqual(TEST_JOB); - }); + describe("search", () => { + beforeEach(() => { + fixture.detectChanges(); }); - describe('search', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - it('makes expected calls', () => { - spyOn(component, 'loadDataWithState').and.callThrough(); - component.search('searchValue'); - expect(component.clrGridUIState.search).toBe('searchValue'); - expect(component.loadDataWithState).toHaveBeenCalled(); - }); + it("makes expected calls", () => { + spyOn(component, "loadDataWithState").and.callThrough(); + component.search("searchValue"); + expect(component.clrGridUIState.search).toBe("searchValue"); + expect(component.loadDataWithState).toHaveBeenCalled(); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.ts index 4d5ea360f0..0f886ba7d9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/data-jobs-explore-grid.component.ts @@ -3,88 +3,109 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, ElementRef, HostBinding, Inject, Input, OnInit } from '@angular/core'; -import { DOCUMENT, Location } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; +import { + Component, + ElementRef, + HostBinding, + Inject, + Input, + OnInit, +} from "@angular/core"; +import { DOCUMENT, Location } from "@angular/common"; +import { ActivatedRoute, Router } from "@angular/router"; -import { ComponentService, ErrorHandlerService, NavigationService, RouterService } from '@versatiledatakit/shared'; +import { + ComponentService, + ErrorHandlerService, + NavigationService, + RouterService, +} from "@versatiledatakit/shared"; -import { DATA_PIPELINES_CONFIGS, DataPipelinesConfig, DisplayMode } from '../../../../model'; -import { DataJobsApiService, DataJobsService } from '../../../../services'; +import { + DATA_PIPELINES_CONFIGS, + DataPipelinesConfig, + DisplayMode, +} from "../../../../model"; +import { DataJobsApiService, DataJobsService } from "../../../../services"; -import { DataJobsBaseGridComponent } from '../../../base-grid/data-jobs-base-grid.component'; +import { DataJobsBaseGridComponent } from "../../../base-grid/data-jobs-base-grid.component"; @Component({ - selector: 'lib-data-jobs-explore-grid', - templateUrl: './data-jobs-explore-grid.component.html', - styleUrls: ['./data-jobs-explore-grid.component.scss'] + selector: "lib-data-jobs-explore-grid", + templateUrl: "./data-jobs-explore-grid.component.html", + styleUrls: ["./data-jobs-explore-grid.component.scss"], }) -export class DataJobsExploreGridComponent extends DataJobsBaseGridComponent implements OnInit { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DataJobsExploreGridComponent'; +export class DataJobsExploreGridComponent + extends DataJobsBaseGridComponent + implements OnInit +{ + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DataJobsExploreGridComponent"; - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'DataJobs-ExploreGrid-Component'; + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = + "DataJobs-ExploreGrid-Component"; - //Decorators are not inherited in Angular. If we need @Input() we need to declare it here - @Input() override teamNameFilter: string; - @Input() override displayMode: DisplayMode; + //Decorators are not inherited in Angular. If we need @Input() we need to declare it here + @Input() override teamNameFilter: string; + @Input() override displayMode: DisplayMode; - @HostBinding('attr.data-cy') attributeDataCy = 'data-pipelines-explore-data-jobs'; + @HostBinding("attr.data-cy") attributeDataCy = + "data-pipelines-explore-data-jobs"; - readonly uuid = 'DataJobsExploreGridComponent'; + readonly uuid = "DataJobsExploreGridComponent"; - constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - routerService: RouterService, - dataJobsService: DataJobsService, - dataJobsApiService: DataJobsApiService, - errorHandlerService: ErrorHandlerService, - location: Location, - router: Router, - elementRef: ElementRef, - @Inject(DOCUMENT) document: Document, - @Inject(DATA_PIPELINES_CONFIGS) - dataPipelinesModuleConfig: DataPipelinesConfig - ) { - super( - componentService, - navigationService, - activatedRoute, - routerService, - dataJobsService, - dataJobsApiService, - errorHandlerService, - location, - router, - elementRef, - document, - dataPipelinesModuleConfig, - 'explore_data_jobs_grid_user_config', - { - hiddenColumns: { - description: true, - lastExecutionDuration: true, - successRate: true, - nextRun: true, - lastDeployedDate: true, - lastDeployedBy: true, - source: true, - logsUrl: true, - jobPythonVersion: true - } - }, - DataJobsExploreGridComponent.CLASS_NAME - ); - } + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + routerService: RouterService, + dataJobsService: DataJobsService, + dataJobsApiService: DataJobsApiService, + errorHandlerService: ErrorHandlerService, + location: Location, + router: Router, + elementRef: ElementRef, + @Inject(DOCUMENT) document: Document, + @Inject(DATA_PIPELINES_CONFIGS) + dataPipelinesModuleConfig: DataPipelinesConfig, + ) { + super( + componentService, + navigationService, + activatedRoute, + routerService, + dataJobsService, + dataJobsApiService, + errorHandlerService, + location, + router, + elementRef, + document, + dataPipelinesModuleConfig, + "explore_data_jobs_grid_user_config", + { + hiddenColumns: { + description: true, + lastExecutionDuration: true, + successRate: true, + nextRun: true, + lastDeployedDate: true, + lastDeployedBy: true, + source: true, + logsUrl: true, + jobPythonVersion: true, + }, + }, + DataJobsExploreGridComponent.CLASS_NAME, + ); + } - showTeamsColumn() { - return this.dataPipelinesModuleConfig?.exploreConfig?.showTeamsColumn; - } + showTeamsColumn() { + return this.dataPipelinesModuleConfig?.exploreConfig?.showTeamsColumn; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/index.ts index f20e16104a..f417a7165b 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/components/grid/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-explore-grid.component'; +export * from "./data-jobs-explore-grid.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.css b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.css index c77bb25ec5..2e9e5e6a33 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.css +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.css @@ -4,7 +4,7 @@ */ .page-container { - display: grid; - height: 100%; - grid-template-rows: 40px 1fr; + display: grid; + height: 100%; + grid-template-rows: 40px 1fr; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.html index a8f8b89523..033161048e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.html @@ -4,10 +4,10 @@ -->
-
-

- Explore Data Jobs -

-
- +
+

+ Explore Data Jobs +

+
+
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.spec.ts index 77ae8bb92f..df8e9c57f4 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.spec.ts @@ -3,24 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { DataJobsExplorePageComponent } from './data-jobs-explore-page.component'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { DataJobsExplorePageComponent } from "./data-jobs-explore-page.component"; -describe('DataJobsExploreComponent', () => { - let component: DataJobsExplorePageComponent; - let fixture: ComponentFixture; +describe("DataJobsExploreComponent", () => { + let component: DataJobsExplorePageComponent; + let fixture: ComponentFixture; - beforeEach(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DataJobsExplorePageComponent] - }); - fixture = TestBed.createComponent(DataJobsExplorePageComponent); - component = fixture.componentInstance; + beforeEach(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DataJobsExplorePageComponent], }); + fixture = TestBed.createComponent(DataJobsExplorePageComponent); + component = fixture.componentInstance; + }); - it('can load instance', () => { - expect(component).toBeTruthy(); - }); + it("can load instance", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.ts index b1d3aaeb0f..2f5097d3d7 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/data-jobs-explore-page.component.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; @Component({ - selector: 'lib-data-jobs-explore', - templateUrl: './data-jobs-explore-page.component.html', - styleUrls: ['./data-jobs-explore-page.component.css'] + selector: "lib-data-jobs-explore", + templateUrl: "./data-jobs-explore-page.component.html", + styleUrls: ["./data-jobs-explore-page.component.css"], }) export class DataJobsExplorePageComponent {} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/index.ts index 159400d344..29ae022b3f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-explore-page.component'; +export * from "./data-jobs-explore-page.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/public-api.ts index a265ad53d3..ce933d6a97 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-explore/public-api.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; -export * from './components/grid'; +export * from "./index"; +export * from "./components/grid"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.html index 68afd46072..0a28004394 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.html @@ -6,599 +6,558 @@
- -
- -
- -
- -
- -
- -
- -
- -
-
- -
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ - Job name + + + Team name + + + + - Job name - - - Team name - - - - - Description - - - - - - Deployment Status - - - - - - - - - Python Version - - - - - Last Execution End (UTC) - - - - Last Execution Duration - - - - Last Execution Status - - - - - - - - - - - - Success rate - - - - - Calculating up to 336 executions from last 14 days - - - - - Schedule (in UTC) - - - Next run (UTC) - - - - - Last Deployed (UTC) - - - - Last Deployed By - - - - - Notifications - - - - - Source - - - - - Logs - - - - Details - - - - {{ job.jobName }} - - - {{ job.config?.team }} - - - {{ job.config?.description | words : 8 }} - - - - - - - - {{ job.deployments ? job.deployments[0]?.jobPythonVersion : - null }} - - - - {{ job.deployments ? (job.deployments[0]?.lastExecutionTime - | date : "MMM d, y, hh:mm a" : "utc") : null }} - - - - {{ job.deployments ? - (job.deployments[0]?.lastExecutionDuration | formatDuration) - : null }} - - - - - - - {{ job.deployments | executionSuccessRate }} - - - {{ job.config?.schedule?.scheduleCron | formatSchedule : "" - }} - - - - - {{ job.config?.schedule?.nextRunEpochSeconds | - parseEpoch | date : "MMM d, y, hh:mm a" : "UTC" }} - - - - - {{ job.deployments && job.deployments[0]?.lastDeployedDate ? - (job.deployments[0]?.lastDeployedDate | date : "MMM d, y, - hh:mm a" : "UTC") : null }} - - - - {{ job.deployments ? job.deployments[0]?.lastDeployedBy : - null }} - - - - - - - - -
-
-
- - - -
-
-
- - - - - - - - - - - - -
- - - - -
-
No data jobs created!
- - Learn - about Data Jobs - -
-
- No data jobs that match with - {{ clrGridUIState.search }} - criteria -
-
-
-
- - - - Data Jobs per page - {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 - }} of {{ pagination.totalItems }} Data Jobs - - -
-
+ Description + + + + + + Deployment Status + + + + + + + + + Python Version + + + + + Last Execution End (UTC) + + + + Last Execution Duration + + + + Last Execution Status + + + + + + + + + + + + Success rate + + + + + Calculating up to 336 executions from last 14 days + + + + + Schedule (in UTC) + + + Next run (UTC) + + + + + Last Deployed (UTC) + + + + Last Deployed By + + + + + Notifications + + + + + Source + + + + + Logs + + + + Details + + + + {{ job.jobName }} + + + {{ + job.config?.team + }} + + + {{ job.config?.description | words: 8 }} + + + + + + + + {{ job.deployments ? job.deployments[0]?.jobPythonVersion : null }} + + + + {{ + job.deployments + ? (job.deployments[0]?.lastExecutionTime + | date: "MMM d, y, hh:mm a" : "utc") + : null + }} + + + + {{ + job.deployments + ? (job.deployments[0]?.lastExecutionDuration | formatDuration) + : null + }} + + + + + + + {{ job.deployments | executionSuccessRate }} + + + {{ job.config?.schedule?.scheduleCron | formatSchedule: "" }} + + + + + {{ + job.config?.schedule?.nextRunEpochSeconds + | parseEpoch + | date: "MMM d, y, hh:mm a" : "UTC" + }} + + + + + {{ + job.deployments && job.deployments[0]?.lastDeployedDate + ? (job.deployments[0]?.lastDeployedDate + | date + : "MMM d, y, + hh:mm a" + : "UTC") + : null + }} + + + + {{ job.deployments ? job.deployments[0]?.lastDeployedBy : null }} + + + + + + + + +
+
+
+ + + +
+
+
+ + + + + + + + + + + + +
+ + + + +
+
No data jobs created!
+ + Learn about Data Jobs + +
+
+ No data jobs that match with + {{ clrGridUIState.search }} + criteria +
+
+
+
+ + + + Data Jobs per page + {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of + {{ pagination.totalItems }} Data Jobs + + + +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.scss index 055071330c..eb073da8aa 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.scss @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import '../../../base-grid/data-jobs-base-grid.component'; +@import "../../../base-grid/data-jobs-base-grid.component"; clr-datagrid clr-dg-placeholder { - .msg-btn-placeholder { - display: flex; - flex-direction: column; - align-items: center; - } + .msg-btn-placeholder { + display: flex; + flex-direction: column; + align-items: center; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.spec.ts index 2218beb4dd..e24ba3ffb2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.spec.ts @@ -3,420 +3,459 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Location } from '@angular/common'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Location } from "@angular/common"; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from "@angular/core/testing"; -import { of, Subject } from 'rxjs'; +import { of, Subject } from "rxjs"; -import { ClrDatagridStateInterface } from '@clr/angular'; +import { ClrDatagridStateInterface } from "@clr/angular"; import { - ASC, - CallFake, - ComponentModel, - ComponentService, - ComponentStateImpl, - DESC, - ErrorHandlerService, - generateErrorCodes, - NavigationService, - RouterService, - RouterState, - RouteState, - ToastService -} from '@versatiledatakit/shared'; - -import { ExtractJobStatusPipe } from '../../../../shared/pipes'; - -import { DataJobsApiService, DataJobsService } from '../../../../services'; - -import { DATA_PIPELINES_CONFIGS, DisplayMode } from '../../../../model'; - -import { TASK_LOAD_JOBS_STATE } from '../../../../state/tasks'; -import { LOAD_JOBS_ERROR_CODES } from '../../../../state/error-codes'; - -import { DataJobsManageGridComponent } from './data-jobs-manage-grid.component'; - -describe('DataJobsManageGridComponent', () => { - let componentServiceStub: jasmine.SpyObj; - let navigationServiceStub: jasmine.SpyObj; - let routerServiceStub: jasmine.SpyObj; - let dataJobsServiceStub: jasmine.SpyObj; - let toastServiceStub: jasmine.SpyObj; - let errorHandlerServiceStub: jasmine.SpyObj; - - let componentModelStub: ComponentModel; - let component: DataJobsManageGridComponent; - let fixture: ComponentFixture; - - const TEST_DEPLOYMENT = { - id: 'id001', - enabled: true - }; - - const TEST_JOB = { - jobName: 'job001', - config: { - description: 'description001', - team: 'testTeam' + ASC, + CallFake, + ComponentModel, + ComponentService, + ComponentStateImpl, + DESC, + ErrorHandlerService, + generateErrorCodes, + NavigationService, + RouterService, + RouterState, + RouteState, + ToastService, +} from "@versatiledatakit/shared"; + +import { ExtractJobStatusPipe } from "../../../../shared/pipes"; + +import { DataJobsApiService, DataJobsService } from "../../../../services"; + +import { DATA_PIPELINES_CONFIGS, DisplayMode } from "../../../../model"; + +import { TASK_LOAD_JOBS_STATE } from "../../../../state/tasks"; +import { LOAD_JOBS_ERROR_CODES } from "../../../../state/error-codes"; + +import { DataJobsManageGridComponent } from "./data-jobs-manage-grid.component"; + +describe("DataJobsManageGridComponent", () => { + let componentServiceStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let routerServiceStub: jasmine.SpyObj; + let dataJobsServiceStub: jasmine.SpyObj; + let toastServiceStub: jasmine.SpyObj; + let errorHandlerServiceStub: jasmine.SpyObj; + + let componentModelStub: ComponentModel; + let component: DataJobsManageGridComponent; + let fixture: ComponentFixture; + + const TEST_DEPLOYMENT = { + id: "id001", + enabled: true, + }; + + const TEST_JOB = { + jobName: "job001", + config: { + description: "description001", + team: "testTeam", + }, + deployments: [TEST_DEPLOYMENT], + }; + + let teamSubject: Subject; + let teamSubjectSubscribeSpy: jasmine.Spy; + + const EMPTY_TEAM_NAME_FILTER = ""; + + beforeAll(() => { + // This hack is needed in order to prevent tests to reload browser. + // Browser reloading stops Tests execution in the same browser, and need to restart execution from beginning. + window.onbeforeunload = () => "Stop browser from reload!"; + }); + + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["init", "getModel", "idle", "update"], + ); + navigationServiceStub = jasmine.createSpyObj( + "navigationService", + ["navigateTo", "navigateBack"], + ); + routerServiceStub = jasmine.createSpyObj("routerService", [ + "getState", + "get", + ]); + dataJobsServiceStub = jasmine.createSpyObj( + "dataJobsService", + [ + "loadJobs", + "notifyForRunningJobExecutionId", + "notifyForJobExecutions", + "notifyForTeamImplicitly", + "getNotifiedForRunningJobExecutionId", + "getNotifiedForJobExecutions", + "getNotifiedForTeamImplicitly", + ], + ); + toastServiceStub = jasmine.createSpyObj("toastService", [ + "show", + ]); + errorHandlerServiceStub = jasmine.createSpyObj( + "errorHandlerService", + ["processError", "handleError"], + ); + + teamSubject = new Subject(); + teamSubjectSubscribeSpy = spyOn(teamSubject, "subscribe").and.callThrough(); + + const activatedRouteStub = () => ({ + queryParams: { + subscribe: CallFake, + }, + snapshot: null, + }); + const dataJobsApiServiceStub = () => ({ + getAllJobs: () => ({ + subscribe: CallFake, + }), + getJobDetails: () => ({ + subscribe: () => CallFake, + }), + getJobExecutions: () => ({ + subscribe: () => CallFake, + }), + updateDataJobStatus: () => ({ + subscribe: () => { + return new Subject(); }, - deployments: [TEST_DEPLOYMENT] - }; - - let teamSubject: Subject; - let teamSubjectSubscribeSpy: jasmine.Spy; - - const EMPTY_TEAM_NAME_FILTER = ''; - - beforeAll(() => { - // This hack is needed in order to prevent tests to reload browser. - // Browser reloading stops Tests execution in the same browser, and need to restart execution from beginning. - window.onbeforeunload = () => 'Stop browser from reload!'; + }), + executeDataJob: () => ({ + subscribe: () => { + return new Subject(); + }, + }), + }); + const locationStub = () => ({ + path: () => "Test", + go: () => CallFake, + }); + const routerStub = () => ({ + url: "/explore/data-jobs", }); - beforeEach(() => { - componentServiceStub = jasmine.createSpyObj('componentService', ['init', 'getModel', 'idle', 'update']); - navigationServiceStub = jasmine.createSpyObj('navigationService', ['navigateTo', 'navigateBack']); - routerServiceStub = jasmine.createSpyObj('routerService', ['getState', 'get']); - dataJobsServiceStub = jasmine.createSpyObj('dataJobsService', [ - 'loadJobs', - 'notifyForRunningJobExecutionId', - 'notifyForJobExecutions', - 'notifyForTeamImplicitly', - 'getNotifiedForRunningJobExecutionId', - 'getNotifiedForJobExecutions', - 'getNotifiedForTeamImplicitly' - ]); - toastServiceStub = jasmine.createSpyObj('toastService', ['show']); - errorHandlerServiceStub = jasmine.createSpyObj('errorHandlerService', ['processError', 'handleError']); - - teamSubject = new Subject(); - teamSubjectSubscribeSpy = spyOn(teamSubject, 'subscribe').and.callThrough(); - - const activatedRouteStub = () => ({ - queryParams: { - subscribe: CallFake + const dataJobsApiServiceStubTemp = jasmine.createSpyObj( + "dataJobsApiServiceStub", + ["getJobs"], + ); + + generateErrorCodes(dataJobsApiServiceStubTemp, [ + "getJobs", + "getJobDetails", + "getJobExecutions", + ]); + + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE] = + dataJobsApiServiceStubTemp.errorCodes.getJobs; + + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DataJobsManageGridComponent, ExtractJobStatusPipe], + providers: [ + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => ({ + defaultOwnerTeamName: "all", + manageConfig: { + filterByTeamName: true, + showTeamsColumn: true, + allowExecuteNow: true, + displayMode: DisplayMode.COMPACT, + selectedTeamNameObservable: teamSubject, }, - snapshot: null - }); - const dataJobsApiServiceStub = () => ({ - getAllJobs: () => ({ - subscribe: CallFake - }), - getJobDetails: () => ({ - subscribe: () => CallFake - }), - getJobExecutions: () => ({ - subscribe: () => CallFake - }), - updateDataJobStatus: () => ({ - subscribe: () => { - return new Subject(); - } - }), - executeDataJob: () => ({ - subscribe: () => { - return new Subject(); - } - }) - }); - const locationStub = () => ({ - path: () => 'Test', - go: () => CallFake - }); - const routerStub = () => ({ - url: '/explore/data-jobs' - }); - - const dataJobsApiServiceStubTemp = jasmine.createSpyObj('dataJobsApiServiceStub', ['getJobs']); - - generateErrorCodes(dataJobsApiServiceStubTemp, ['getJobs', 'getJobDetails', 'getJobExecutions']); - - LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE] = dataJobsApiServiceStubTemp.errorCodes.getJobs; - - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DataJobsManageGridComponent, ExtractJobStatusPipe], - providers: [ - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => ({ - defaultOwnerTeamName: 'all', - manageConfig: { - filterByTeamName: true, - showTeamsColumn: true, - allowExecuteNow: true, - displayMode: DisplayMode.COMPACT, - selectedTeamNameObservable: teamSubject - } - }) - }, - { provide: RouterService, useValue: routerServiceStub }, - { provide: ComponentService, useValue: componentServiceStub }, - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: ActivatedRoute, useFactory: activatedRouteStub }, - { provide: DataJobsService, useValue: dataJobsServiceStub }, - { provide: ToastService, useValue: toastServiceStub }, - { - provide: DataJobsApiService, - useFactory: dataJobsApiServiceStub - }, - { provide: Location, useFactory: locationStub }, - { provide: Router, useFactory: routerStub }, - { - provide: ErrorHandlerService, - useValue: errorHandlerServiceStub - } - ] - }); - - componentModelStub = ComponentModel.of(ComponentStateImpl.of({}), RouterState.of(RouteState.empty(), 1)); - componentServiceStub.init.and.returnValue(of(componentModelStub)); - componentServiceStub.getModel.and.returnValue(of(componentModelStub)); - routerServiceStub.getState.and.returnValue(new Subject()); - routerServiceStub.get.and.returnValue(new Subject()); - - fixture = TestBed.createComponent(DataJobsManageGridComponent); - component = fixture.componentInstance; - component.selectedJob = TEST_JOB; - component.model = componentModelStub; + }), + }, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ComponentService, useValue: componentServiceStub }, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: ActivatedRoute, useFactory: activatedRouteStub }, + { provide: DataJobsService, useValue: dataJobsServiceStub }, + { provide: ToastService, useValue: toastServiceStub }, + { + provide: DataJobsApiService, + useFactory: dataJobsApiServiceStub, + }, + { provide: Location, useFactory: locationStub }, + { provide: Router, useFactory: routerStub }, + { + provide: ErrorHandlerService, + useValue: errorHandlerServiceStub, + }, + ], }); - it('can load instance', () => { - expect(component).toBeTruthy(); + componentModelStub = ComponentModel.of( + ComponentStateImpl.of({}), + RouterState.of(RouteState.empty(), 1), + ); + componentServiceStub.init.and.returnValue(of(componentModelStub)); + componentServiceStub.getModel.and.returnValue(of(componentModelStub)); + routerServiceStub.getState.and.returnValue(new Subject()); + routerServiceStub.get.and.returnValue(new Subject()); + + fixture = TestBed.createComponent(DataJobsManageGridComponent); + component = fixture.componentInstance; + component.selectedJob = TEST_JOB; + component.model = componentModelStub; + }); + + it("can load instance", () => { + expect(component).toBeTruthy(); + }); + + describe("editJob", () => { + it("skips editJob for undefined job", () => { + // Given + const navigateToSpy = spyOn(component, "navigateTo").and.callFake( + CallFake, + ); + component.selectedJob = null; + + // When + component.navigateToJobDetails(); + + // Then + expect(component.selectedJob).toBeNull(); + expect(navigateToSpy).not.toHaveBeenCalled(); }); - describe('editJob', () => { - it('skips editJob for undefined job', () => { - // Given - const navigateToSpy = spyOn(component, 'navigateTo').and.callFake(CallFake); - component.selectedJob = null; - - // When - component.navigateToJobDetails(); - - // Then - expect(component.selectedJob).toBeNull(); - expect(navigateToSpy).not.toHaveBeenCalled(); - }); - - it('opens editJob for valid job', () => { - // Given - const navigateToSpy = spyOn(component, 'navigateTo').and.returnValue(Promise.resolve(true)); - component.selectedJob = null; - component.ngOnInit(); - - // When - component.navigateToJobDetails(TEST_JOB); - - // Then - expect(component.selectedJob).toBeDefined(); - expect(navigateToSpy).toHaveBeenCalledWith({ - '$.team': 'testTeam', - '$.job': 'job001' - }); - }); + it("opens editJob for valid job", () => { + // Given + const navigateToSpy = spyOn(component, "navigateTo").and.returnValue( + Promise.resolve(true), + ); + component.selectedJob = null; + component.ngOnInit(); + + // When + component.navigateToJobDetails(TEST_JOB); + + // Then + expect(component.selectedJob).toBeDefined(); + expect(navigateToSpy).toHaveBeenCalledWith({ + "$.team": "testTeam", + "$.job": "job001", + }); }); + }); - describe('ngOnInit', () => { - it('makes expected calls', () => { - component.ngOnInit(); - expect(componentServiceStub.init).toHaveBeenCalled(); - expect(componentServiceStub.getModel).toHaveBeenCalled(); - }); + describe("ngOnInit", () => { + it("makes expected calls", () => { + component.ngOnInit(); + expect(componentServiceStub.init).toHaveBeenCalled(); + expect(componentServiceStub.getModel).toHaveBeenCalled(); }); + }); - describe('showTeamsColumn', () => { - it('returns correct value', () => { - expect(component.showTeamsColumn()).toBeTrue(); - }); + describe("showTeamsColumn", () => { + it("returns correct value", () => { + expect(component.showTeamsColumn()).toBeTrue(); }); - - describe('onJobStatusChange', () => { - it('makes expected calls for empty SelectedJobDeployment', () => { - component.selectedJob.deployments = []; - spyOn(component, 'extractSelectedJobDeployment').and.callThrough(); - spyOn(console, 'log').and.callThrough(); - component.onJobStatusChange(); - expect(component.extractSelectedJobDeployment).toHaveBeenCalled(); - expect(console.log).toHaveBeenCalled(); - }); - - it('makes expected calls for valid SelectedJobDeployment', () => { - component.selectedJob.deployments = [TEST_DEPLOYMENT]; - const dataJobSServiceStub: DataJobsApiService = fixture.debugElement.injector.get(DataJobsApiService); - spyOn(component, 'extractSelectedJobDeployment').and.callThrough(); - spyOn(dataJobSServiceStub, 'updateDataJobStatus').and.callThrough(); - component.onJobStatusChange(); - expect(component.extractSelectedJobDeployment).toHaveBeenCalled(); - expect(dataJobSServiceStub.updateDataJobStatus).toHaveBeenCalled(); - }); + }); + + describe("onJobStatusChange", () => { + it("makes expected calls for empty SelectedJobDeployment", () => { + component.selectedJob.deployments = []; + spyOn(component, "extractSelectedJobDeployment").and.callThrough(); + spyOn(console, "log").and.callThrough(); + component.onJobStatusChange(); + expect(component.extractSelectedJobDeployment).toHaveBeenCalled(); + expect(console.log).toHaveBeenCalled(); }); - describe('onExecuteDataJob', () => { - it('makes expected calls', () => { - component.selectedJob = TEST_JOB; - component.selectedJob.deployments = [TEST_DEPLOYMENT]; - const dataJobSServiceStub: DataJobsApiService = fixture.debugElement.injector.get(DataJobsApiService); - spyOn(component, 'extractSelectedJobDeployment').and.callThrough(); - spyOn(dataJobSServiceStub, 'executeDataJob').and.callThrough(); - component.onExecuteDataJob(); - expect(component.extractSelectedJobDeployment).toHaveBeenCalled(); - expect(dataJobSServiceStub.executeDataJob).toHaveBeenCalled(); - }); + it("makes expected calls for valid SelectedJobDeployment", () => { + component.selectedJob.deployments = [TEST_DEPLOYMENT]; + const dataJobSServiceStub: DataJobsApiService = + fixture.debugElement.injector.get(DataJobsApiService); + spyOn(component, "extractSelectedJobDeployment").and.callThrough(); + spyOn(dataJobSServiceStub, "updateDataJobStatus").and.callThrough(); + component.onJobStatusChange(); + expect(component.extractSelectedJobDeployment).toHaveBeenCalled(); + expect(dataJobSServiceStub.updateDataJobStatus).toHaveBeenCalled(); }); - - describe('executeDataJob', () => { - it('sets the expected options', () => { - component.executeDataJob(); - expect(component.confirmExecuteNowOptions.message).toBeDefined(); - expect(component.confirmExecuteNowOptions.infoText).toBeDefined(); - expect(component.confirmExecuteNowOptions.opened).toBeTrue(); - }); + }); + + describe("onExecuteDataJob", () => { + it("makes expected calls", () => { + component.selectedJob = TEST_JOB; + component.selectedJob.deployments = [TEST_DEPLOYMENT]; + const dataJobSServiceStub: DataJobsApiService = + fixture.debugElement.injector.get(DataJobsApiService); + spyOn(component, "extractSelectedJobDeployment").and.callThrough(); + spyOn(dataJobSServiceStub, "executeDataJob").and.callThrough(); + component.onExecuteDataJob(); + expect(component.extractSelectedJobDeployment).toHaveBeenCalled(); + expect(dataJobSServiceStub.executeDataJob).toHaveBeenCalled(); }); - - describe('enable', () => { - it('sets the expected options', () => { - component.enable(); - expect(component.confirmStatusOptions.message).toBeDefined(); - expect(component.confirmStatusOptions.infoText).toBeDefined(); - expect(component.confirmStatusOptions.opened).toBeTrue(); - }); + }); + + describe("executeDataJob", () => { + it("sets the expected options", () => { + component.executeDataJob(); + expect(component.confirmExecuteNowOptions.message).toBeDefined(); + expect(component.confirmExecuteNowOptions.infoText).toBeDefined(); + expect(component.confirmExecuteNowOptions.opened).toBeTrue(); }); - - describe('disable', () => { - it('sets the expected options', () => { - component.disable(); - expect(component.confirmStatusOptions.message).toBeDefined(); - expect(component.confirmStatusOptions.infoText).toBeDefined(); - expect(component.confirmStatusOptions.opened).toBeTrue(); - }); + }); + + describe("enable", () => { + it("sets the expected options", () => { + component.enable(); + expect(component.confirmStatusOptions.message).toBeDefined(); + expect(component.confirmStatusOptions.infoText).toBeDefined(); + expect(component.confirmStatusOptions.opened).toBeTrue(); }); + }); + + describe("disable", () => { + it("sets the expected options", () => { + component.disable(); + expect(component.confirmStatusOptions.message).toBeDefined(); + expect(component.confirmStatusOptions.infoText).toBeDefined(); + expect(component.confirmStatusOptions.opened).toBeTrue(); + }); + }); - describe('component initialization', () => { - it('reads manageConfig filterByTeamName properly', () => { - expect(component.filterByTeamName).toBeTrue(); - }); - - it('reads manageConfig displayMode properly', () => { - expect(component.displayMode).toEqual(DisplayMode.COMPACT); - }); + describe("component initialization", () => { + it("reads manageConfig filterByTeamName properly", () => { + expect(component.filterByTeamName).toBeTrue(); + }); - it('reads manageConfig selectedTeamNameObservable properly', () => { - expect(teamSubjectSubscribeSpy).toHaveBeenCalled(); - }); + it("reads manageConfig displayMode properly", () => { + expect(component.displayMode).toEqual(DisplayMode.COMPACT); }); - describe('resetTeamNameFilter', () => { - it('resets the TeamNameFilter', () => { - component.resetTeamNameFilter(); - expect(component.teamNameFilter).toEqual(EMPTY_TEAM_NAME_FILTER); - }); + it("reads manageConfig selectedTeamNameObservable properly", () => { + expect(teamSubjectSubscribeSpy).toHaveBeenCalled(); }); + }); - describe('Methods::', () => { - describe('|loadDataWithState|', () => { - it('should verify will append on model team filter when default team exist', fakeAsync(() => { - // Given - const state: ClrDatagridStateInterface = { - filters: null, - sort: null, - page: { - size: 10, - current: 2 - } - }; - component.ngOnInit(); - teamSubject.next('teamA'); - - tick(100); - - // When - component.loadDataWithState(state); - - tick(600); - - // Then - const filter = componentModelStub.getComponentState().filter; - expect(filter.criteria.length).toEqual(1); - expect(filter.criteria[0]).toEqual({ - property: 'config.team', - pattern: 'teamA', - sort: null - }); - })); - - it('should verify will append on model filter and sorting and default team filter', fakeAsync(() => { - // Given - const state: ClrDatagridStateInterface = { - filters: [{ property: 'deployments.enabled', value: 'true' }], - sort: { by: 'jobName', reverse: false }, - page: { - size: 10, - current: 2 - } - }; - component.ngOnInit(); - teamSubject.next('teamB'); - - tick(100); - - // When - component.loadDataWithState(state); - - tick(600); - - // Then - const filter = componentModelStub.getComponentState().filter; - expect(filter.criteria).toEqual([ - { property: 'config.team', pattern: 'teamB', sort: null }, - { - property: 'deployments.enabled', - pattern: 'true', - sort: null - }, - { property: 'jobName', pattern: null, sort: ASC } - ]); - })); - - it('should verify will append on model filter and sorting and no default team filter', fakeAsync(() => { - // Given - const state: ClrDatagridStateInterface = { - filters: [{ property: 'deployments.enabled', value: 'false' }], - sort: { by: 'config.description', reverse: true }, - page: { - size: 10, - current: 2 - } - }; - component.filterByTeamName = false; - component.ngOnInit(); - - tick(100); - - // When - component.loadDataWithState(state); - - tick(600); - - // Then - const filter = componentModelStub.getComponentState().filter; - console.log(filter.criteria); - expect(filter.criteria).toEqual([ - { - property: 'deployments.enabled', - pattern: 'false', - sort: null - }, - { - property: 'config.description', - pattern: null, - sort: DESC - } - ]); - })); + describe("resetTeamNameFilter", () => { + it("resets the TeamNameFilter", () => { + component.resetTeamNameFilter(); + expect(component.teamNameFilter).toEqual(EMPTY_TEAM_NAME_FILTER); + }); + }); + + describe("Methods::", () => { + describe("|loadDataWithState|", () => { + it("should verify will append on model team filter when default team exist", fakeAsync(() => { + // Given + const state: ClrDatagridStateInterface = { + filters: null, + sort: null, + page: { + size: 10, + current: 2, + }, + }; + component.ngOnInit(); + teamSubject.next("teamA"); + + tick(100); + + // When + component.loadDataWithState(state); + + tick(600); + + // Then + const filter = componentModelStub.getComponentState().filter; + expect(filter.criteria.length).toEqual(1); + expect(filter.criteria[0]).toEqual({ + property: "config.team", + pattern: "teamA", + sort: null, }); + })); + + it("should verify will append on model filter and sorting and default team filter", fakeAsync(() => { + // Given + const state: ClrDatagridStateInterface = { + filters: [{ property: "deployments.enabled", value: "true" }], + sort: { by: "jobName", reverse: false }, + page: { + size: 10, + current: 2, + }, + }; + component.ngOnInit(); + teamSubject.next("teamB"); + + tick(100); + + // When + component.loadDataWithState(state); + + tick(600); + + // Then + const filter = componentModelStub.getComponentState().filter; + expect(filter.criteria).toEqual([ + { property: "config.team", pattern: "teamB", sort: null }, + { + property: "deployments.enabled", + pattern: "true", + sort: null, + }, + { property: "jobName", pattern: null, sort: ASC }, + ]); + })); + + it("should verify will append on model filter and sorting and no default team filter", fakeAsync(() => { + // Given + const state: ClrDatagridStateInterface = { + filters: [{ property: "deployments.enabled", value: "false" }], + sort: { by: "config.description", reverse: true }, + page: { + size: 10, + current: 2, + }, + }; + component.filterByTeamName = false; + component.ngOnInit(); + + tick(100); + + // When + component.loadDataWithState(state); + + tick(600); + + // Then + const filter = componentModelStub.getComponentState().filter; + console.log(filter.criteria); + expect(filter.criteria).toEqual([ + { + property: "deployments.enabled", + pattern: "false", + sort: null, + }, + { + property: "config.description", + pattern: null, + sort: DESC, + }, + ]); + })); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.ts index 68a7a66f42..2329c7903a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/data-jobs-manage-grid.component.ts @@ -3,226 +3,270 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, ElementRef, HostBinding, Inject, OnInit } from '@angular/core'; -import { DOCUMENT, Location } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; -import { HttpErrorResponse } from '@angular/common/http'; +import { + Component, + ElementRef, + HostBinding, + Inject, + OnInit, +} from "@angular/core"; +import { DOCUMENT, Location } from "@angular/common"; +import { ActivatedRoute, Router } from "@angular/router"; +import { HttpErrorResponse } from "@angular/common/http"; import { - ComponentService, - ErrorHandlerService, - NavigationService, - RouterService, - ToastService, - VmwToastType -} from '@versatiledatakit/shared'; + ComponentService, + ErrorHandlerService, + NavigationService, + RouterService, + ToastService, + VmwToastType, +} from "@versatiledatakit/shared"; -import { ErrorUtil } from '../../../../shared/utils'; +import { ErrorUtil } from "../../../../shared/utils"; -import { ConfirmationModalOptions, ModalOptions } from '../../../../shared/model'; +import { + ConfirmationModalOptions, + ModalOptions, +} from "../../../../shared/model"; -import { DATA_PIPELINES_CONFIGS, DataJobStatus, DataPipelinesConfig, ToastDefinitions } from '../../../../model'; -import { DataJobsApiService, DataJobsService } from '../../../../services'; +import { + DATA_PIPELINES_CONFIGS, + DataJobStatus, + DataPipelinesConfig, + ToastDefinitions, +} from "../../../../model"; +import { DataJobsApiService, DataJobsService } from "../../../../services"; -import { ClrGridUIState, DataJobsBaseGridComponent } from '../../../base-grid/data-jobs-base-grid.component'; +import { + ClrGridUIState, + DataJobsBaseGridComponent, +} from "../../../base-grid/data-jobs-base-grid.component"; @Component({ - selector: 'lib-data-jobs-manage-grid', - templateUrl: './data-jobs-manage-grid.component.html', - styleUrls: ['./data-jobs-manage-grid.component.scss'] + selector: "lib-data-jobs-manage-grid", + templateUrl: "./data-jobs-manage-grid.component.html", + styleUrls: ["./data-jobs-manage-grid.component.scss"], }) -export class DataJobsManageGridComponent extends DataJobsBaseGridComponent implements OnInit { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DataJobsManageGridComponent'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'DataJobs-ManageGrid-Component'; - - @HostBinding('attr.data-cy') attributeDataCy = 'data-pipelines-manage-data-jobs'; - - readonly uuid = 'DataJobsManageGridComponent'; - - confirmStatusOptions: ModalOptions; - confirmExecuteNowOptions: ModalOptions; - - override clrGridDefaultFilter: ClrGridUIState['filter'] = { - deploymentStatus: DataJobStatus.ENABLED - }; - override clrGridDefaultSort: ClrGridUIState['sort'] = { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'deployments.lastExecutionTime': -1 - }; - - dataPipelinesDocumentationUrl: string; - - constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - routerService: RouterService, - dataJobsService: DataJobsService, - dataJobsApiService: DataJobsApiService, - errorHandlerService: ErrorHandlerService, - location: Location, - router: Router, - elementRef: ElementRef, - @Inject(DOCUMENT) document: Document, - @Inject(DATA_PIPELINES_CONFIGS) - dataPipelinesModuleConfig: DataPipelinesConfig, - private readonly toastService: ToastService - ) { - super( - componentService, - navigationService, - activatedRoute, - routerService, - dataJobsService, - dataJobsApiService, - errorHandlerService, - location, - router, - elementRef, - document, - dataPipelinesModuleConfig, - 'manage_data_jobs_grid_user_config', - { - hiddenColumns: { - description: true, - nextRun: true, - lastDeployedDate: true, - lastDeployedBy: true, - notifications: true, - source: true - } - }, - DataJobsManageGridComponent.CLASS_NAME - ); +export class DataJobsManageGridComponent + extends DataJobsBaseGridComponent + implements OnInit +{ + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DataJobsManageGridComponent"; - this.confirmStatusOptions = new ConfirmationModalOptions(); - this.confirmExecuteNowOptions = new ConfirmationModalOptions(); + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = + "DataJobs-ManageGrid-Component"; - this._inputConfig(dataPipelinesModuleConfig); - } + @HostBinding("attr.data-cy") attributeDataCy = + "data-pipelines-manage-data-jobs"; - enable() { - this.confirmStatusOptions.message = `Job ${this.selectedJob.jobName} will be enabled`; - this.confirmStatusOptions.infoText = `Enabling this job means that it will be scheduled for execution`; - this.confirmStatusOptions.opened = true; - } + readonly uuid = "DataJobsManageGridComponent"; + + confirmStatusOptions: ModalOptions; + confirmExecuteNowOptions: ModalOptions; - disable() { - this.confirmStatusOptions.message = `Job ${this.selectedJob.jobName} will be disabled`; - this.confirmStatusOptions.infoText = `Disabling this job means that + override clrGridDefaultFilter: ClrGridUIState["filter"] = { + deploymentStatus: DataJobStatus.ENABLED, + }; + override clrGridDefaultSort: ClrGridUIState["sort"] = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "deployments.lastExecutionTime": -1, + }; + + dataPipelinesDocumentationUrl: string; + + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + routerService: RouterService, + dataJobsService: DataJobsService, + dataJobsApiService: DataJobsApiService, + errorHandlerService: ErrorHandlerService, + location: Location, + router: Router, + elementRef: ElementRef, + @Inject(DOCUMENT) document: Document, + @Inject(DATA_PIPELINES_CONFIGS) + dataPipelinesModuleConfig: DataPipelinesConfig, + private readonly toastService: ToastService, + ) { + super( + componentService, + navigationService, + activatedRoute, + routerService, + dataJobsService, + dataJobsApiService, + errorHandlerService, + location, + router, + elementRef, + document, + dataPipelinesModuleConfig, + "manage_data_jobs_grid_user_config", + { + hiddenColumns: { + description: true, + nextRun: true, + lastDeployedDate: true, + lastDeployedBy: true, + notifications: true, + source: true, + }, + }, + DataJobsManageGridComponent.CLASS_NAME, + ); + + this.confirmStatusOptions = new ConfirmationModalOptions(); + this.confirmExecuteNowOptions = new ConfirmationModalOptions(); + + this._inputConfig(dataPipelinesModuleConfig); + } + + enable() { + this.confirmStatusOptions.message = `Job ${this.selectedJob.jobName} will be enabled`; + this.confirmStatusOptions.infoText = `Enabling this job means that it will be scheduled for execution`; + this.confirmStatusOptions.opened = true; + } + + disable() { + this.confirmStatusOptions.message = `Job ${this.selectedJob.jobName} will be disabled`; + this.confirmStatusOptions.infoText = `Disabling this job means that it will NOT be scheduled for execution anymore`; - this.confirmStatusOptions.opened = true; - } + this.confirmStatusOptions.opened = true; + } - onJobStatusChange() { - const selectedJobDeployment = this.extractSelectedJobDeployment(); - if (!selectedJobDeployment) { - console.log('Status update action will not be performed for job with no deployments.'); - return; - } - - this.subscriptions.push( - this.dataJobsApiService - .updateDataJobStatus( - this.selectedJob.config?.team, - this.selectedJob.jobName, - selectedJobDeployment.id, - !selectedJobDeployment.enabled - ) - .subscribe({ - next: () => { - selectedJobDeployment.enabled = !selectedJobDeployment.enabled; - - const state = selectedJobDeployment.enabled ? 'enabled' : 'disabled'; - - this.toastService.show({ - type: VmwToastType.INFO, - title: `Status update completed`, - description: `Data job "${this.selectedJob.jobName}" successfully ${state}` - }); - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - title: `Updating status for Data job "${this.selectedJob?.jobName}" failed` - }); - } - }) - ); + onJobStatusChange() { + const selectedJobDeployment = this.extractSelectedJobDeployment(); + if (!selectedJobDeployment) { + console.log( + "Status update action will not be performed for job with no deployments.", + ); + return; } - executeDataJob() { - this.confirmExecuteNowOptions.message = `Job ${this.selectedJob.jobName} will be queued for execution.`; - this.confirmExecuteNowOptions.infoText = `Confirming will result in immediate data job execution.`; - this.confirmExecuteNowOptions.opened = true; - } + this.subscriptions.push( + this.dataJobsApiService + .updateDataJobStatus( + this.selectedJob.config?.team, + this.selectedJob.jobName, + selectedJobDeployment.id, + !selectedJobDeployment.enabled, + ) + .subscribe({ + next: () => { + selectedJobDeployment.enabled = !selectedJobDeployment.enabled; - onExecuteDataJob() { - this.subscriptions.push( - this.dataJobsApiService - .executeDataJob(this.selectedJob.config?.team, this.selectedJob.jobName, this.extractSelectedJobDeployment().id) - .subscribe({ - next: () => { - this.toastService.show(ToastDefinitions.successfullyRanJob(this.selectedJob.jobName)); - }, - error: (error: unknown) => { - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error), { - title: - (error as HttpErrorResponse)?.status === 409 - ? 'Failed, Data job is already executing' - : 'Failed to queue Data job for execution' - }); - } - }) - ); - } + const state = selectedJobDeployment.enabled + ? "enabled" + : "disabled"; - resetTeamNameFilter() { - this.teamNameFilter = ''; - } + this.toastService.show({ + type: VmwToastType.INFO, + title: `Status update completed`, + description: `Data job "${this.selectedJob.jobName}" successfully ${state}`, + }); + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + title: `Updating status for Data job "${this.selectedJob?.jobName}" failed`, + }, + ); + }, + }), + ); + } + + executeDataJob() { + this.confirmExecuteNowOptions.message = `Job ${this.selectedJob.jobName} will be queued for execution.`; + this.confirmExecuteNowOptions.infoText = `Confirming will result in immediate data job execution.`; + this.confirmExecuteNowOptions.opened = true; + } + + onExecuteDataJob() { + this.subscriptions.push( + this.dataJobsApiService + .executeDataJob( + this.selectedJob.config?.team, + this.selectedJob.jobName, + this.extractSelectedJobDeployment().id, + ) + .subscribe({ + next: () => { + this.toastService.show( + ToastDefinitions.successfullyRanJob(this.selectedJob.jobName), + ); + }, + error: (error: unknown) => { + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + { + title: + (error as HttpErrorResponse)?.status === 409 + ? "Failed, Data job is already executing" + : "Failed to queue Data job for execution", + }, + ); + }, + }), + ); + } - showTeamsColumn() { - return this.dataPipelinesModuleConfig?.manageConfig?.showTeamsColumn; + resetTeamNameFilter() { + this.teamNameFilter = ""; + } + + showTeamsColumn() { + return this.dataPipelinesModuleConfig?.manageConfig?.showTeamsColumn; + } + + extractSelectedJobDeployment() { + return this.selectedJob?.deployments[ + this.selectedJob?.deployments?.length - 1 + ]; + } + + private _inputConfig(dataPipelinesModuleConfig: DataPipelinesConfig) { + if (dataPipelinesModuleConfig.manageConfig?.filterByTeamName) { + this.filterByTeamName = + dataPipelinesModuleConfig.manageConfig?.filterByTeamName; } - extractSelectedJobDeployment() { - return this.selectedJob?.deployments[this.selectedJob?.deployments?.length - 1]; + if (dataPipelinesModuleConfig.manageConfig?.displayMode) { + this.displayMode = dataPipelinesModuleConfig.manageConfig?.displayMode; } - private _inputConfig(dataPipelinesModuleConfig: DataPipelinesConfig) { - if (dataPipelinesModuleConfig.manageConfig?.filterByTeamName) { - this.filterByTeamName = dataPipelinesModuleConfig.manageConfig?.filterByTeamName; - } - - if (dataPipelinesModuleConfig.manageConfig?.displayMode) { - this.displayMode = dataPipelinesModuleConfig.manageConfig?.displayMode; - } - - if (dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable) { - this.subscriptions.push( - dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable.subscribe({ - next: (newTeam) => { - if (newTeam !== this.teamNameFilter) { - this.teamNameFilter = newTeam; - this.refresh(); - } - }, - error: (error: unknown) => { - this.resetTeamNameFilter(); - console.error('Error loading selected team', error); - } - }) - ); - } + if (dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable) { + this.subscriptions.push( + dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable.subscribe( + { + next: (newTeam) => { + if (newTeam !== this.teamNameFilter) { + this.teamNameFilter = newTeam; + this.refresh(); + } + }, + error: (error: unknown) => { + this.resetTeamNameFilter(); + console.error("Error loading selected team", error); + }, + }, + ), + ); + } - if (dataPipelinesModuleConfig?.dataPipelinesDocumentationUrl) { - this.dataPipelinesDocumentationUrl = dataPipelinesModuleConfig.dataPipelinesDocumentationUrl; - } + if (dataPipelinesModuleConfig?.dataPipelinesDocumentationUrl) { + this.dataPipelinesDocumentationUrl = + dataPipelinesModuleConfig.dataPipelinesDocumentationUrl; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/index.ts index 019e9084bd..ab4710a77b 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/components/grid/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-manage-grid.component'; +export * from "./data-jobs-manage-grid.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.css b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.css index c77bb25ec5..2e9e5e6a33 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.css +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.css @@ -4,7 +4,7 @@ */ .page-container { - display: grid; - height: 100%; - grid-template-rows: 40px 1fr; + display: grid; + height: 100%; + grid-template-rows: 40px 1fr; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.html index 6d7e7d60a5..e2989e0ab2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.html @@ -4,10 +4,10 @@ -->
-
-

- Manage Data Jobs -

-
- +
+

+ Manage Data Jobs +

+
+
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.spec.ts index b671cb0cf3..d1101f20e5 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.spec.ts @@ -3,24 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { DataJobsManagePageComponent } from './data-jobs-manage-page.component'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { DataJobsManagePageComponent } from "./data-jobs-manage-page.component"; -describe('DataJobsManageComponent', () => { - let component: DataJobsManagePageComponent; - let fixture: ComponentFixture; +describe("DataJobsManageComponent", () => { + let component: DataJobsManagePageComponent; + let fixture: ComponentFixture; - beforeEach(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DataJobsManagePageComponent] - }); - fixture = TestBed.createComponent(DataJobsManagePageComponent); - component = fixture.componentInstance; + beforeEach(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DataJobsManagePageComponent], }); + fixture = TestBed.createComponent(DataJobsManagePageComponent); + component = fixture.componentInstance; + }); - it('can load instance', () => { - expect(component).toBeTruthy(); - }); + it("can load instance", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.ts index a9364bbf89..0244dd5532 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/data-jobs-manage-page.component.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; @Component({ - selector: 'lib-data-jobs-manage', - templateUrl: './data-jobs-manage-page.component.html', - styleUrls: ['./data-jobs-manage-page.component.css'] + selector: "lib-data-jobs-manage", + templateUrl: "./data-jobs-manage-page.component.html", + styleUrls: ["./data-jobs-manage-page.component.css"], }) export class DataJobsManagePageComponent {} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/index.ts index 7b5f863705..47c22fab79 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-manage-page.component'; +export * from "./data-jobs-manage-page.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/public-api.ts index a265ad53d3..ce933d6a97 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-jobs-manage/public-api.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; -export * from './components/grid'; +export * from "./index"; +export * from "./components/grid"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/public-api.ts index 7d36bb80ef..24c86a4240 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/public-api.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job/public-api'; -export * from './data-jobs-explore/public-api'; -export * from './data-jobs-manage/public-api'; -export * from './widgets/public-api'; +export * from "./data-job/public-api"; +export * from "./data-jobs-explore/public-api"; +export * from "./data-jobs-manage/public-api"; +export * from "./widgets/public-api"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.html index 88687435de..0de9f08d19 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.html @@ -4,63 +4,63 @@ --> - We couldn't find any failed executions! + We couldn't find any failed executions! - Most recent failed executions - Last 24h (UTC time) - + Most recent failed executions - Last 24h (UTC time) + - Status - - - - + Status + + + + - End (UTC) - + End (UTC) + - + + + {{ jobExecution.id }} + + + + + + {{ jobExecution.endTimeFormatted }} - - - {{ jobExecution.id }} - - - - - - {{ jobExecution.endTimeFormatted }} - + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.scss index 7fe6714b55..2d6a109bd8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.scss @@ -4,56 +4,56 @@ */ .datagrid-executions-widget-table { - height: 300px; - - ::ng-deep .datagrid-table { - width: 100%; - - .header-title { - .datagrid-column-separator { - display: none; - } - } - - .datagrid-row-scrollable { - width: 100%; - } - - .job-name-column { - width: 60% !important; - } - - .status-column { - width: 20% !important; - padding-left: 0px !important; - white-space: nowrap; - } - - .time-column { - width: 20% !important; - min-width: 120px; - } - - clr-dg-cell { - .btn { - width: 100% !important; - text-align: left; - } - } + height: 300px; + + ::ng-deep .datagrid-table { + width: 100%; + + .header-title { + .datagrid-column-separator { + display: none; + } + } + + .datagrid-row-scrollable { + width: 100%; + } + + .job-name-column { + width: 60% !important; + } + + .status-column { + width: 20% !important; + padding-left: 0px !important; + white-space: nowrap; + } + + .time-column { + width: 20% !important; + min-width: 120px; + } + + clr-dg-cell { + .btn { + width: 100% !important; + text-align: left; + } } + } } clr-dg-cell .btn { - margin: 0 !important; - text-transform: none !important; - padding: 0.3rem 0.3rem 0.3rem; + margin: 0 !important; + text-transform: none !important; + padding: 0.3rem 0.3rem 0.3rem; } .no-padding { - padding-bottom: 0px; - padding-top: 0px; + padding-bottom: 0px; + padding-top: 0px; } .hide { - display: none; + display: none; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.ts index b52849e54f..03240a76da 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/data-jobs-executions-widget.component.ts @@ -3,57 +3,66 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + SimpleChanges, +} from "@angular/core"; -import { NavigationService } from '@versatiledatakit/shared'; +import { NavigationService } from "@versatiledatakit/shared"; -import { DataJob, DataJobExecution } from '../../../model'; +import { DataJob, DataJobExecution } from "../../../model"; -import { GridDataJobExecution } from '../../data-job/pages/executions'; +import { GridDataJobExecution } from "../../data-job/pages/executions"; @Component({ - selector: 'lib-data-jobs-executions-widget', - templateUrl: './data-jobs-executions-widget.component.html', - styleUrls: ['./data-jobs-executions-widget.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: "lib-data-jobs-executions-widget", + templateUrl: "./data-jobs-executions-widget.component.html", + styleUrls: ["./data-jobs-executions-widget.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataJobsExecutionsWidgetComponent implements OnChanges { - @Input() manageLink: string; - @Input() allJobs: DataJob[]; - @Input() jobExecutions: GridDataJobExecution[] = []; + @Input() manageLink: string; + @Input() allJobs: DataJob[]; + @Input() jobExecutions: GridDataJobExecution[] = []; - readonly uuid = 'DataJobsExecutionsWidgetComponent'; + readonly uuid = "DataJobsExecutionsWidgetComponent"; - loading = true; + loading = true; - constructor(private readonly navigationService: NavigationService) {} + constructor(private readonly navigationService: NavigationService) {} - /** - * ** NgFor elements tracking function. - */ - trackByFn(index: number, execution: DataJobExecution): string { - return `${index}|${execution?.id}`; - } + /** + * ** NgFor elements tracking function. + */ + trackByFn(index: number, execution: DataJobExecution): string { + return `${index}|${execution?.id}`; + } - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['jobExecutions'] !== undefined && changes['jobExecutions'].currentValue !== undefined) { - this.loading = false; - } + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges) { + if ( + changes["jobExecutions"] !== undefined && + changes["jobExecutions"].currentValue !== undefined + ) { + this.loading = false; } + } + + navigateToJobExecutions(job?: DataJobExecution): void { + const dataJob = this.allJobs.find((el) => el.jobName === job.jobName); + let link = this.manageLink; + link = link.replace("{team}", dataJob.config?.team); + link = link.replace("{data-job}", dataJob.jobName); + link = link + "/executions"; - navigateToJobExecutions(job?: DataJobExecution): void { - const dataJob = this.allJobs.find((el) => el.jobName === job.jobName); - let link = this.manageLink; - link = link.replace('{team}', dataJob.config?.team); - link = link.replace('{data-job}', dataJob.jobName); - link = link + '/executions'; - - if (dataJob) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.navigationService.navigate(link); - } + if (dataJob) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.navigationService.navigate(link); } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/index.ts index 671fe26929..f8d55aa476 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-executions-widget/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-executions-widget.component'; +export * from "./data-jobs-executions-widget.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.html index be2d9263ce..cd645929d8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.html @@ -4,45 +4,45 @@ --> - We couldn't find any failed jobs! + We couldn't find any failed jobs! - Jobs with Failing executions - Last 24h - + Jobs with Failing executions - Last 24h + - Failed executions - + Failed executions + - - - - {{ dataJob.jobName }} - - + + + + {{ dataJob.jobName }} + + - - {{ dataJob.failedTotal }} - - + + {{ + dataJob.failedTotal + }} + + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.scss index 1fdf7f924f..3fe9e36671 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.scss @@ -4,54 +4,54 @@ */ .datagrid-failed-executions-widget { - height: 300px; - - ::ng-deep .datagrid-table { - width: 100%; - - .header-title { - .datagrid-column-separator { - display: none; - } - } - - .datagrid-row-scrollable { - width: 100%; - } - - .job-name-column { - width: 70% !important; - } - - .center { - width: 30% !important; - text-align: center; - } - - clr-dg-cell { - .btn { - width: 100% !important; - text-align: left; - } - } + height: 300px; + + ::ng-deep .datagrid-table { + width: 100%; + + .header-title { + .datagrid-column-separator { + display: none; + } + } + + .datagrid-row-scrollable { + width: 100%; + } + + .job-name-column { + width: 70% !important; + } + + .center { + width: 30% !important; + text-align: center; + } + + clr-dg-cell { + .btn { + width: 100% !important; + text-align: left; + } } + } } clr-dg-cell .btn { - margin: 0 !important; - text-transform: none !important; - padding: 0.3rem 0.3rem 0.3rem; + margin: 0 !important; + text-transform: none !important; + padding: 0.3rem 0.3rem 0.3rem; } .no-padding { - padding-bottom: 0px; - padding-top: 0px; + padding-bottom: 0px; + padding-top: 0px; } .custom-label { - width: 50px; + width: 50px; } .label-column { - display: none; + display: none; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.ts index bc0c66d969..bef557e180 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/data-jobs-failed-widget.component.ts @@ -3,74 +3,82 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + SimpleChanges, +} from "@angular/core"; -import { NavigationService } from '@versatiledatakit/shared'; +import { NavigationService } from "@versatiledatakit/shared"; -import { DataJob, DataJobExecutions } from '../../../model'; +import { DataJob, DataJobExecutions } from "../../../model"; -import { GridDataJobExecution } from '../../data-job/pages/executions'; +import { GridDataJobExecution } from "../../data-job/pages/executions"; interface DataJobGrid extends DataJob { - failedTotal?: number; + failedTotal?: number; } @Component({ - selector: 'lib-data-jobs-failed-widget', - templateUrl: './data-jobs-failed-widget.component.html', - styleUrls: ['./data-jobs-failed-widget.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: "lib-data-jobs-failed-widget", + templateUrl: "./data-jobs-failed-widget.component.html", + styleUrls: ["./data-jobs-failed-widget.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DataJobsFailedWidgetComponent implements OnChanges { - @Input() manageLink: string; - @Input() allJobs: DataJob[]; - @Input() jobExecutions: GridDataJobExecution[] = []; + @Input() manageLink: string; + @Input() allJobs: DataJob[]; + @Input() jobExecutions: GridDataJobExecution[] = []; - readonly uuid = 'DataJobsFailedWidgetComponent'; + readonly uuid = "DataJobsFailedWidgetComponent"; - loading = true; - dataJobs: DataJobGrid[] = []; + loading = true; + dataJobs: DataJobGrid[] = []; - constructor(private readonly navigationService: NavigationService) {} + constructor(private readonly navigationService: NavigationService) {} - /** - * ** NgFor elements tracking function. - */ - trackByFn(index: number, dataJob: DataJob): string { - return `${index}|${dataJob?.jobName}`; - } + /** + * ** NgFor elements tracking function. + */ + trackByFn(index: number, dataJob: DataJob): string { + return `${index}|${dataJob?.jobName}`; + } - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['jobExecutions'] !== undefined) { - this.dataJobs = []; - (changes['jobExecutions'].currentValue as DataJobExecutions).forEach((element) => { - const temp = this.dataJobs.find((i) => i.jobName === element.jobName); - if (!temp) { - this.dataJobs.push({ - jobName: element.jobName, - failedTotal: 1 - } as DataJobGrid); - } else { - temp.failedTotal++; - } - }); - this.loading = false; - } + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges) { + if (changes["jobExecutions"] !== undefined) { + this.dataJobs = []; + (changes["jobExecutions"].currentValue as DataJobExecutions).forEach( + (element) => { + const temp = this.dataJobs.find((i) => i.jobName === element.jobName); + if (!temp) { + this.dataJobs.push({ + jobName: element.jobName, + failedTotal: 1, + } as DataJobGrid); + } else { + temp.failedTotal++; + } + }, + ); + this.loading = false; } + } - navigateToJobDetails(job?: DataJob): void { - const dataJob = this.allJobs.find((el) => el.jobName === job.jobName); - let link = this.manageLink; - link = link.replace('{team}', dataJob.config?.team); - link = link.replace('{data-job}', job.jobName); - link = link + '/details'; + navigateToJobDetails(job?: DataJob): void { + const dataJob = this.allJobs.find((el) => el.jobName === job.jobName); + let link = this.manageLink; + link = link.replace("{team}", dataJob.config?.team); + link = link.replace("{data-job}", job.jobName); + link = link + "/details"; - if (dataJob) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.navigationService.navigate(link); - } + if (dataJob) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.navigationService.navigate(link); } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/index.ts index ecdb6dc3ba..0623601e97 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-failed-widget/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-failed-widget.component'; +export * from "./data-jobs-failed-widget.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.html index 15e33b7ebc..c973919456 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.html @@ -4,74 +4,67 @@ -->
-
-
-
-

- - Data Jobs - -

- -
-

- Data Jobs help Data Engineers develop, deploy, run, and manage - data processing workloads -

-
+
+
+
+

+ + Data Jobs + +

+ +
+

+ Data Jobs help Data Engineers develop, deploy, run, and manage data + processing workloads +

+
-
-
-
- -
-
- -
-
- -
-
- -
- - -
-
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
-
+ +
+ diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.scss index 779c7a1f87..e03d524eeb 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.scss @@ -3,274 +3,274 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import '../variables.scss'; +@import "../variables.scss"; .widget { - position: relative; - height: 100%; + position: relative; + height: 100%; - .widget-section-container { - padding: 1.2rem; - height: 100%; - border-radius: 0.2rem; - border: none; - box-shadow: - 0 3px 10px 0 rgba(0, 0, 0, 0.1), - 0 3px 5px 0 rgba(0, 0, 0, 0.09); - position: relative; - overflow: hidden; - ::ng-deep .datagrid-table-wrapper { - max-width: 98.6% !important; - } + .widget-section-container { + padding: 1.2rem; + height: 100%; + border-radius: 0.2rem; + border: none; + box-shadow: + 0 3px 10px 0 rgba(0, 0, 0, 0.1), + 0 3px 5px 0 rgba(0, 0, 0, 0.09); + position: relative; + overflow: hidden; + ::ng-deep .datagrid-table-wrapper { + max-width: 98.6% !important; } + } } ::ng-deep .fade-to-dark.dark { - .widget-card { - --label-color-error: #f27963; - --label-color-success: #5eb715; - --header-background: #28404d; - --widget-border: 1px solid #28404d; - --widget-clickable-background: #324f62; - --widget-details-background: #21333b; - --widget-details-hover-background: #1b2a32; - --widget-details-color: #b3b3b3; - --widget-details-border: 2px solid #28404d; - --widget-icon-backgound-color: #194b70; - } + .widget-card { + --label-color-error: #f27963; + --label-color-success: #5eb715; + --header-background: #28404d; + --widget-border: 1px solid #28404d; + --widget-clickable-background: #324f62; + --widget-details-background: #21333b; + --widget-details-hover-background: #1b2a32; + --widget-details-color: #b3b3b3; + --widget-details-border: 2px solid #28404d; + --widget-icon-backgound-color: #194b70; + } } .widget-card { - --label-color-error: #f35e44; - --label-color-success: #5aa220; - --header-background: #{$color-white}; - --widget-border: 1px solid #e3f5fc; - --widget-clickable-background: #e8e8e8; - --widget-details-background: #{$color-white}; - --widget-details-hover-background: #d8e3e9; - --widget-details-color: #8c8c8c; - --widget-details-border: 2px solid #e3f5fc; - --widget-icon-backgound-color: #0072a3; - - .label-error { - color: var(--label-color-error); - } + --label-color-error: #f35e44; + --label-color-success: #5aa220; + --header-background: #{$color-white}; + --widget-border: 1px solid #e3f5fc; + --widget-clickable-background: #e8e8e8; + --widget-details-background: #{$color-white}; + --widget-details-hover-background: #d8e3e9; + --widget-details-color: #8c8c8c; + --widget-details-border: 2px solid #e3f5fc; + --widget-icon-backgound-color: #0072a3; + + .label-error { + color: var(--label-color-error); + } + + .label-success { + color: var(--label-color-success); + } + + .widget-footer { + position: relative; + min-height: 25px; + margin: 1.3rem -1.2rem -1.2rem; + border-top: var(--widget-border); - .label-success { - color: var(--label-color-success); + a { + margin: 0 0 0 0.5rem; + padding: 0px; } + } - .widget-footer { - position: relative; - min-height: 25px; - margin: 1.3rem -1.2rem -1.2rem; - border-top: var(--widget-border); + .widget-header { + background: var(--header-background); + margin: -1.2rem -1.2rem 0 -1.2rem; + padding: 0.8rem; + border-radius: 0.1rem 0.1rem 0 0; + position: relative; + + .widget-header-title-refresh-button { + display: flex; + align-items: center; - a { - margin: 0 0 0 0.5rem; - padding: 0px; + .header-title { + margin-top: 0; + font-weight: 200; + + clr-icon { + margin-top: -2px; } + } + } + p { + margin-top: 0.4rem; + line-height: 0.8rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } + } + + .widget-container { + z-index: 100; + + .widget-values { + line-height: 0.8rem; + display: grid; + grid-template: ". . ." 1fr / 1fr 1fr 1fr; + margin-left: -1.2rem; + margin-right: -1.2rem; + margin-bottom: -1.2rem; + row-gap: 1.2rem; + border-top: var(--widget-border); + + .widget-value { + display: block; + text-align: center; + background: var(--widget-details-background); - .widget-header { - background: var(--header-background); - margin: -1.2rem -1.2rem 0 -1.2rem; - padding: 0.8rem; - border-radius: 0.1rem 0.1rem 0 0; - position: relative; + &:not(:first-child) { + border-left: var(--widget-border); + } - .widget-header-title-refresh-button { - display: flex; - align-items: center; + &:not(:last-child) { + border-right: var(--widget-border); + } - .header-title { - margin-top: 0; - font-weight: 200; + padding: 0.6rem; - clr-icon { - margin-top: -2px; - } - } + .widget-title { + margin-bottom: 0.2rem; + margin-top: 0.2rem; + font-size: 1.2rem; + font-weight: 500; + display: block; } - p { - margin-top: 0.4rem; - line-height: 0.8rem; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + + .widget-text { + font-size: 0.55rem; + display: block; + line-height: 0.6rem; } - } - .widget-container { - z-index: 100; - - .widget-values { - line-height: 0.8rem; - display: grid; - grid-template: '. . .' 1fr / 1fr 1fr 1fr; - margin-left: -1.2rem; - margin-right: -1.2rem; - margin-bottom: -1.2rem; - row-gap: 1.2rem; - border-top: var(--widget-border); - - .widget-value { - display: block; - text-align: center; - background: var(--widget-details-background); - - &:not(:first-child) { - border-left: var(--widget-border); - } - - &:not(:last-child) { - border-right: var(--widget-border); - } - - padding: 0.6rem; - - .widget-title { - margin-bottom: 0.2rem; - margin-top: 0.2rem; - font-size: 1.2rem; - font-weight: 500; - display: block; - } - - .widget-text { - font-size: 0.55rem; - display: block; - line-height: 0.6rem; - } - - &.widget-clickable { - cursor: pointer; - - &:hover { - background: var(--widget-clickable-background); - } - } - - &.show-details { - background: var(--widget-clickable-background); - - .widget-text { - font-weight: bold; - } - - position: relative; - } - } + &.widget-clickable { + cursor: pointer; + + &:hover { + background: var(--widget-clickable-background); + } } + + &.show-details { + background: var(--widget-clickable-background); + + .widget-text { + font-weight: bold; + } + + position: relative; + } + } + } + } + + .widget-details { + z-index: 1; + height: 0; + margin: 1.2rem -1.2rem -1.2rem -1.2rem; + transition: height 0.3s; + overflow: hidden; + background: var(--widget-details-background); + + &.show-details { + height: 240px; + border-top: var(--widget-details-border); } - .widget-details { - z-index: 1; - height: 0; - margin: 1.2rem -1.2rem -1.2rem -1.2rem; - transition: height 0.3s; - overflow: hidden; - background: var(--widget-details-background); + .no-issues { + height: 100%; + width: 100%; + text-align: center; + margin-top: 1.2rem; + + .no-issues-img { + display: block; + height: 120px; + margin: 0 auto; + } + + .no-issues-text { + color: var(--widget-details-color); + } + } - &.show-details { - height: 240px; - border-top: var(--widget-details-border); + .data-details { + height: 240px; + position: relative; + + :host ::ng-deep clr-datagrid { + height: 100%; + + .datagrid { + margin-top: 0; + flex-basis: 0; + border: 0; + + .datagrid-row { + border-top: none; + border-bottom: none; + } + + .datagrid-placeholder-container { + border-top: none; + } } - .no-issues { - height: 100%; - width: 100%; - text-align: center; - margin-top: 1.2rem; + .datagrid-footer { + padding: 0.1rem 0.5rem; + border-right: none; + border-left: none; + border-bottom: none; + } + } - .no-issues-img { - display: block; - height: 120px; - margin: 0 auto; - } + .data-row { + display: block; + margin-top: 0; + position: relative; + padding-left: 0.2rem; - .no-issues-text { - color: var(--widget-details-color); - } + &.clickable { + cursor: pointer; } - .data-details { - height: 240px; - position: relative; - - :host ::ng-deep clr-datagrid { - height: 100%; - - .datagrid { - margin-top: 0; - flex-basis: 0; - border: 0; - - .datagrid-row { - border-top: none; - border-bottom: none; - } - - .datagrid-placeholder-container { - border-top: none; - } - } - - .datagrid-footer { - padding: 0.1rem 0.5rem; - border-right: none; - border-left: none; - border-bottom: none; - } - } + .data-title { + line-height: 0.6rem; + font-weight: 500; + display: block; + margin-top: 0; + position: relative; - .data-row { - display: block; - margin-top: 0; - position: relative; - padding-left: 0.2rem; - - &.clickable { - cursor: pointer; - } - - .data-title { - line-height: 0.6rem; - font-weight: 500; - display: block; - margin-top: 0; - position: relative; - - .btn { - margin: 0; - } - - .title-icon { - float: left; - width: 32px; - display: flex; - justify-content: center; - - lib-status-cell { - width: 15px; - display: flex; - justify-content: center; - } - } - } - - .data-description { - display: block; - margin-top: 0; - color: var(--widget-details-color); - line-height: 1rem; - - clr-icon { - margin-top: -2px; - } - } + .btn { + margin: 0; + } + + .title-icon { + float: left; + width: 32px; + display: flex; + justify-content: center; + + lib-status-cell { + width: 15px; + display: flex; + justify-content: center; } + } + } + + .data-description { + display: block; + margin-top: 0; + color: var(--widget-details-color); + line-height: 1rem; + + clr-icon { + margin-top: -2px; + } } + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.ts index 739644d8f0..ae85657806 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/data-jobs-health-panel.component.ts @@ -3,257 +3,300 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; -import { DatePipe } from '@angular/common'; -import { ActivatedRoute } from '@angular/router'; +import { + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output, +} from "@angular/core"; +import { DatePipe } from "@angular/common"; +import { ActivatedRoute } from "@angular/router"; import { - ApiPredicate, - CollectionsUtil, - ComponentModel, - ComponentService, - DESC, - ErrorRecord, - NavigationService, - OnTaurusModelChange, - OnTaurusModelError, - OnTaurusModelInit, - OnTaurusModelLoad, - RouterService, - TaurusBaseComponent -} from '@versatiledatakit/shared'; + ApiPredicate, + CollectionsUtil, + ComponentModel, + ComponentService, + DESC, + ErrorRecord, + NavigationService, + OnTaurusModelChange, + OnTaurusModelError, + OnTaurusModelInit, + OnTaurusModelLoad, + RouterService, + TaurusBaseComponent, +} from "@versatiledatakit/shared"; -import { ErrorUtil } from '../../../shared/utils'; +import { ErrorUtil } from "../../../shared/utils"; -import { DataJobsService } from '../../../services'; +import { DataJobsService } from "../../../services"; import { - DATA_PIPELINES_CONFIGS, - DataJob, - DataJobExecutionFilter, - DataJobExecutionOrder, - DataJobExecutions, - DataJobExecutionStatus, - DataJobPage, - DataPipelinesConfig, - FILTER_REQ_PARAM, - JOB_EXECUTIONS_DATA_KEY, - JOB_NAME_REQ_PARAM, - JOBS_DATA_KEY, - ORDER_REQ_PARAM, - TEAM_NAME_REQ_PARAM -} from '../../../model'; + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobExecutionFilter, + DataJobExecutionOrder, + DataJobExecutions, + DataJobExecutionStatus, + DataJobPage, + DataPipelinesConfig, + FILTER_REQ_PARAM, + JOB_EXECUTIONS_DATA_KEY, + JOB_NAME_REQ_PARAM, + JOBS_DATA_KEY, + ORDER_REQ_PARAM, + TEAM_NAME_REQ_PARAM, +} from "../../../model"; -import { TASK_LOAD_JOB_EXECUTIONS, TASK_LOAD_JOBS_STATE } from '../../../state/tasks'; -import { LOAD_JOB_ERROR_CODES, LOAD_JOBS_ERROR_CODES } from '../../../state/error-codes'; +import { + TASK_LOAD_JOB_EXECUTIONS, + TASK_LOAD_JOBS_STATE, +} from "../../../state/tasks"; +import { + LOAD_JOB_ERROR_CODES, + LOAD_JOBS_ERROR_CODES, +} from "../../../state/error-codes"; -import { DataJobExecutionToGridDataJobExecution, GridDataJobExecution } from '../../data-job/pages/executions'; +import { + DataJobExecutionToGridDataJobExecution, + GridDataJobExecution, +} from "../../data-job/pages/executions"; enum State { - loading = 'loading', - ready = 'ready', - empty = 'empty', - error = 'error' + loading = "loading", + ready = "ready", + empty = "empty", + error = "error", } @Component({ - selector: 'lib-data-jobs-health-panel', - templateUrl: './data-jobs-health-panel.component.html', - styleUrls: ['./data-jobs-health-panel.component.scss'], - providers: [DatePipe] + selector: "lib-data-jobs-health-panel", + templateUrl: "./data-jobs-health-panel.component.html", + styleUrls: ["./data-jobs-health-panel.component.scss"], + providers: [DatePipe], }) export class DataJobsHealthPanelComponent - extends TaurusBaseComponent - implements OnInit, OnTaurusModelInit, OnTaurusModelLoad, OnTaurusModelChange, OnTaurusModelError + extends TaurusBaseComponent + implements + OnInit, + OnTaurusModelInit, + OnTaurusModelLoad, + OnTaurusModelChange, + OnTaurusModelError { - @Input() manageLink: string; - @Output() componentStateEvent = new EventEmitter(); + @Input() manageLink: string; + @Output() componentStateEvent = new EventEmitter(); - readonly uuid = 'DataJobsHealthPanelComponent'; + readonly uuid = "DataJobsHealthPanelComponent"; - loadingJobs = true; - loadingExecutions = true; + loadingJobs = true; + loadingExecutions = true; - teamName: string; - loading = true; - dataJobs: DataJob[]; - jobExecutions: GridDataJobExecution[] = []; + teamName: string; + loading = true; + dataJobs: DataJob[]; + jobExecutions: GridDataJobExecution[] = []; - /** - * ** Flag that indicates there is jobs executions load error. - */ - isComponentInErrorState = false; + /** + * ** Flag that indicates there is jobs executions load error. + */ + isComponentInErrorState = false; - /** - * ** Array of error code patterns that component should listen for in errors store. - */ - listenForErrorPatterns: string[] = [ - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS].All, - LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE].All - ]; + /** + * ** Array of error code patterns that component should listen for in errors store. + */ + listenForErrorPatterns: string[] = [ + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS].All, + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE].All, + ]; - constructor( - componentService: ComponentService, - navigationService: NavigationService, - activatedRoute: ActivatedRoute, - private readonly routerService: RouterService, - private readonly dataJobsService: DataJobsService, - private readonly datePipe: DatePipe, - @Inject(DATA_PIPELINES_CONFIGS) - public readonly dataPipelinesModuleConfig: DataPipelinesConfig - ) { - super(componentService, navigationService, activatedRoute); - } + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + private readonly routerService: RouterService, + private readonly dataJobsService: DataJobsService, + private readonly datePipe: DatePipe, + @Inject(DATA_PIPELINES_CONFIGS) + public readonly dataPipelinesModuleConfig: DataPipelinesConfig, + ) { + super(componentService, navigationService, activatedRoute); + } - fetchDataJobs(): void { - this.loadingJobs = true; - const filters: ApiPredicate[] = []; + fetchDataJobs(): void { + this.loadingJobs = true; + const filters: ApiPredicate[] = []; - if (this.teamName) { - filters.push({ - property: 'config.team', - pattern: this.teamName, - sort: null - }); - } - - this.dataJobsService.loadJobs( - this.model - .withRequestParam(TEAM_NAME_REQ_PARAM, 'no-team-specified') - .withRequestParam(JOB_NAME_REQ_PARAM, '') - .withFilter(filters) - .withRequestParam(ORDER_REQ_PARAM, { - property: 'startTime', - direction: DESC - }) - .withPage(1, 1000) - ); + if (this.teamName) { + filters.push({ + property: "config.team", + pattern: this.teamName, + sort: null, + }); } - fetchDataJobExecutions(): void { - this.loadingExecutions = true; - const d = new Date(); - d.setDate(d.getDate() - 1); - this.dataJobsService.loadJobExecutions( - this.model - .withRequestParam(TEAM_NAME_REQ_PARAM, 'no-team-specified') - .withRequestParam(JOB_NAME_REQ_PARAM, '') - .withRequestParam(FILTER_REQ_PARAM, { - statusIn: [DataJobExecutionStatus.USER_ERROR, DataJobExecutionStatus.PLATFORM_ERROR], - startTimeGte: d, - teamNameIn: this.teamName ? [this.teamName] : [] - } as DataJobExecutionFilter) - .withRequestParam(ORDER_REQ_PARAM, { - property: 'startTime', - direction: DESC - } as DataJobExecutionOrder) - ); - } + this.dataJobsService.loadJobs( + this.model + .withRequestParam(TEAM_NAME_REQ_PARAM, "no-team-specified") + .withRequestParam(JOB_NAME_REQ_PARAM, "") + .withFilter(filters) + .withRequestParam(ORDER_REQ_PARAM, { + property: "startTime", + direction: DESC, + }) + .withPage(1, 1000), + ); + } - /** - * @inheritDoc - */ - onModelInit(): void { - this._subscribeForTeamChange(); - this._emitNewState(); - } + fetchDataJobExecutions(): void { + this.loadingExecutions = true; + const d = new Date(); + d.setDate(d.getDate() - 1); + this.dataJobsService.loadJobExecutions( + this.model + .withRequestParam(TEAM_NAME_REQ_PARAM, "no-team-specified") + .withRequestParam(JOB_NAME_REQ_PARAM, "") + .withRequestParam(FILTER_REQ_PARAM, { + statusIn: [ + DataJobExecutionStatus.USER_ERROR, + DataJobExecutionStatus.PLATFORM_ERROR, + ], + startTimeGte: d, + teamNameIn: this.teamName ? [this.teamName] : [], + } as DataJobExecutionFilter) + .withRequestParam(ORDER_REQ_PARAM, { + property: "startTime", + direction: DESC, + } as DataJobExecutionOrder), + ); + } - /** - * @inheritDoc - */ - onModelLoad(): void { - this.loading = false; - } + /** + * @inheritDoc + */ + onModelInit(): void { + this._subscribeForTeamChange(); + this._emitNewState(); + } - /** - * @inheritDoc - */ - onModelChange(model: ComponentModel, task: string): void { - if (task === TASK_LOAD_JOB_EXECUTIONS) { - const executions: DataJobExecutions = model.getComponentState().data.get(JOB_EXECUTIONS_DATA_KEY); - if (executions) { - const remappedExecutions = DataJobExecutionToGridDataJobExecution.convertToDataJobExecution(this.datePipe)([...executions]); - this.jobExecutions = remappedExecutions.filter((ex) => ex.status !== DataJobExecutionStatus.SUCCEEDED); - this.loadingExecutions = false; - } - } else if (task === TASK_LOAD_JOBS_STATE) { - const componentState = model.getComponentState(); - const dataJobsData: DataJobPage = componentState.data.get(JOBS_DATA_KEY); + /** + * @inheritDoc + */ + onModelLoad(): void { + this.loading = false; + } - this.dataJobs = CollectionsUtil.isArray(dataJobsData?.content) ? [...dataJobsData?.content] : []; - this.loadingJobs = false; - } + /** + * @inheritDoc + */ + onModelChange(model: ComponentModel, task: string): void { + if (task === TASK_LOAD_JOB_EXECUTIONS) { + const executions: DataJobExecutions = model + .getComponentState() + .data.get(JOB_EXECUTIONS_DATA_KEY); + if (executions) { + const remappedExecutions = + DataJobExecutionToGridDataJobExecution.convertToDataJobExecution( + this.datePipe, + )([...executions]); + this.jobExecutions = remappedExecutions.filter( + (ex) => ex.status !== DataJobExecutionStatus.SUCCEEDED, + ); + this.loadingExecutions = false; + } + } else if (task === TASK_LOAD_JOBS_STATE) { + const componentState = model.getComponentState(); + const dataJobsData: DataJobPage = componentState.data.get(JOBS_DATA_KEY); - this._emitNewState(); + this.dataJobs = CollectionsUtil.isArray(dataJobsData?.content) + ? [...dataJobsData?.content] + : []; + this.loadingJobs = false; } - /** - * @inheritDoc - */ - onModelError(model: ComponentModel, task: string, newErrorRecords: ErrorRecord[]): void { - newErrorRecords.forEach((errorRecord) => { - const error = ErrorUtil.extractError(errorRecord.error); + this._emitNewState(); + } - if (task === TASK_LOAD_JOB_EXECUTIONS) { - this.jobExecutions = []; - this.loadingExecutions = false; - } else if (task === TASK_LOAD_JOBS_STATE) { - this.loadingJobs = false; - } + /** + * @inheritDoc + */ + onModelError( + model: ComponentModel, + task: string, + newErrorRecords: ErrorRecord[], + ): void { + newErrorRecords.forEach((errorRecord) => { + const error = ErrorUtil.extractError(errorRecord.error); - // don't show toast message, only log to console, logic for component is to stay hidden when there is no data are there is error + if (task === TASK_LOAD_JOB_EXECUTIONS) { + this.jobExecutions = []; + this.loadingExecutions = false; + } else if (task === TASK_LOAD_JOBS_STATE) { + this.loadingJobs = false; + } - console.error(error); - }); - } + // don't show toast message, only log to console, logic for component is to stay hidden when there is no data are there is error - /** - * @inheritDoc - */ - override ngOnInit(): void { - // attach listener to ErrorStore and listen for Errors change - this.errors.onChange((store) => { - // if there is record for listened error code patterns set component in error state - this.isComponentInErrorState = store.hasCodePattern(...this.listenForErrorPatterns); - }); + console.error(error); + }); + } - super.ngOnInit(); - } + /** + * @inheritDoc + */ + override ngOnInit(): void { + // attach listener to ErrorStore and listen for Errors change + this.errors.onChange((store) => { + // if there is record for listened error code patterns set component in error state + this.isComponentInErrorState = store.hasCodePattern( + ...this.listenForErrorPatterns, + ); + }); - private _emitNewState() { - if (this.loadingJobs || this.loadingExecutions) { - this.componentStateEvent.emit(State.loading); - } else if (this.jobExecutions.length === 0 && this.dataJobs.length === 0) { - this.componentStateEvent.emit(State.empty); - } else if (this.isComponentInErrorState) { - this.componentStateEvent.emit(State.error); - } else { - this.componentStateEvent.emit(State.ready); - } + super.ngOnInit(); + } + + private _emitNewState() { + if (this.loadingJobs || this.loadingExecutions) { + this.componentStateEvent.emit(State.loading); + } else if (this.jobExecutions.length === 0 && this.dataJobs.length === 0) { + this.componentStateEvent.emit(State.empty); + } else if (this.isComponentInErrorState) { + this.componentStateEvent.emit(State.error); + } else { + this.componentStateEvent.emit(State.ready); } + } - private _subscribeForTeamChange(): void { - if (this.dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable) { - this.subscriptions.push( - this.dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable.subscribe({ - next: (newTeamName: string) => { - if (newTeamName !== this.teamName) { - if (newTeamName && newTeamName !== '') { - this.teamName = newTeamName; - this.fetchDataJobExecutions(); - this.fetchDataJobs(); - } - } - }, - error: (error: unknown) => { - this.jobExecutions = []; - this.dataJobs = []; - console.error('Error loading selected team', error); - } - }) - ); - } else { - this.fetchDataJobExecutions(); - this.fetchDataJobs(); - } + private _subscribeForTeamChange(): void { + if ( + this.dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable + ) { + this.subscriptions.push( + this.dataPipelinesModuleConfig.manageConfig?.selectedTeamNameObservable.subscribe( + { + next: (newTeamName: string) => { + if (newTeamName !== this.teamName) { + if (newTeamName && newTeamName !== "") { + this.teamName = newTeamName; + this.fetchDataJobExecutions(); + this.fetchDataJobs(); + } + } + }, + error: (error: unknown) => { + this.jobExecutions = []; + this.dataJobs = []; + console.error("Error loading selected team", error); + }, + }, + ), + ); + } else { + this.fetchDataJobExecutions(); + this.fetchDataJobs(); } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/index.ts index 5b02377aa6..457fd0438c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-health-panel/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-health-panel.component'; +export * from "./data-jobs-health-panel.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.html index 3610f7e8e1..50fcdb17b4 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.html @@ -4,224 +4,217 @@ -->
-
-
-

Data Jobs

-

- Data Jobs help Data Engineers develop, deploy, run, and manage - data processing workloads -

-
+
+
+

Data Jobs

+

+ Data Jobs help Data Engineers develop, deploy, run, and manage data + processing workloads +

+
-
-
-
-
- - - - Data Jobs - -
-
-
- - - - Job Executions - last 24 hours -
-
- - - - Failures - last 24 hours -
-
+
+
+
+
+ + + + Data Jobs + +
-
-
-
- -
-
- -
-
- -
-
-
+ + + + Job Executions + last 24 hours
- -
-
+ + -
- - - -
- {{ item.jobName }} - {{ item.startTime | date : "MMM d, y, hh:mm a" }} , - took 12min, {{ item.status }} -
-
-
- Coming Soon! - - - - -
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + + + + + +
+ + + +
+ {{ item.jobName }} + {{ item.startTime | date: "MMM d, y, hh:mm a" }} , took 12min, + {{ item.status }} +
+
+
+ Coming Soon! + + + + +
+
-
- - - -
- {{ item.jobName }} - {{ item.startTime | date : "MMM d, y, hh:mm a" }} , - took 12min, {{ item.status }} -
-
-
- Coming Soon! +
+ + + +
+ {{ item.jobName }} + {{ item.startTime | date: "MMM d, y, hh:mm a" }} , took 12min, + {{ item.status }} +
+
+
+ Coming Soon! - - - - -
-
+ + + + +
+
-
- - Job - Schedule (in UTC) - - -
- {{ item.jobName }} -
-
- - {{ item.config?.schedule?.scheduleCron | formatSchedule : - "Not scheduled" }} - -
- We couldn't find any data jobs, but you can always create - one! +
+ + Job + Schedule (in UTC) + + +
+ {{ item.jobName }} +
+
+ + {{ + item.config?.schedule?.scheduleCron + | formatSchedule: "Not scheduled" + }} + +
+ We couldn't find any data jobs, but you can always create + one! - - - - -
-
+ + + + +
+
-
-
- Loading ... -
+
+
+ Loading ...
+
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.scss index 103deeb8e2..262f0a9d8f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.scss @@ -3,261 +3,261 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import 'variables.scss'; +@import "variables.scss"; ::ng-deep .fade-to-dark.dark { - .widget-card { - --label-color-error: #f27963; - --label-color-success: #5eb715; - --header-background: #28404d; - --widget-border: 1px solid #28404d; - --widget-clickable-background: #324f62; - --widget-details-background: #21333b; - --widget-details-hover-background: #1b2a32; - --widget-details-color: #b3b3b3; - --widget-details-border: 2px solid #28404d; - --widget-icon-backgound-color: #194b70; - } + .widget-card { + --label-color-error: #f27963; + --label-color-success: #5eb715; + --header-background: #28404d; + --widget-border: 1px solid #28404d; + --widget-clickable-background: #324f62; + --widget-details-background: #21333b; + --widget-details-hover-background: #1b2a32; + --widget-details-color: #b3b3b3; + --widget-details-border: 2px solid #28404d; + --widget-icon-backgound-color: #194b70; + } } .widget-card { - --label-color-error: #f35e44; - --label-color-success: #5aa220; - --header-background: #{$color-white}; - --widget-border: 1px solid #e3f5fc; - --widget-clickable-background: #e8e8e8; - --widget-details-background: #{$color-white}; - --widget-details-hover-background: #d8e3e9; - --widget-details-color: #8c8c8c; - --widget-details-border: 2px solid #e3f5fc; - --widget-icon-backgound-color: #0072a3; - - .label-error { - color: var(--label-color-error); + --label-color-error: #f35e44; + --label-color-success: #5aa220; + --header-background: #{$color-white}; + --widget-border: 1px solid #e3f5fc; + --widget-clickable-background: #e8e8e8; + --widget-details-background: #{$color-white}; + --widget-details-hover-background: #d8e3e9; + --widget-details-color: #8c8c8c; + --widget-details-border: 2px solid #e3f5fc; + --widget-icon-backgound-color: #0072a3; + + .label-error { + color: var(--label-color-error); + } + + .label-success { + color: var(--label-color-success); + } + + .widget-footer { + position: relative; + min-height: 25px; + margin: 1.3rem -1.2rem -1.2rem; + border-top: var(--widget-border); + + a { + margin: 0 0 0 0.5rem; + padding: 0px; + } + } + + .widget-header { + background: var(--header-background); + margin: -1.2rem -1.2rem 0 -1.2rem; + padding: 0.8rem; + border-radius: 0.1rem 0.1rem 0 0; + position: relative; + min-height: 96px; + + .header-title { + margin-top: 0; + font-weight: 200; + + clr-icon { + margin-top: -2px; + } } - .label-success { - color: var(--label-color-success); + p { + margin-top: 0.3rem; + line-height: 0.8rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } - .widget-footer { - position: relative; - min-height: 25px; - margin: 1.3rem -1.2rem -1.2rem; - border-top: var(--widget-border); + .header-link { + position: absolute; + top: 0.4rem; + right: 0.3rem; - a { - margin: 0 0 0 0.5rem; - padding: 0px; - } + clr-icon { + margin-top: -0.1rem; + } } + } + + .widget-container { + z-index: 100; + + .widget-values { + line-height: 0.8rem; + display: grid; + grid-template: ". . ." 1fr / 1fr 1fr 1fr; + margin-left: -1.2rem; + margin-right: -1.2rem; + margin-bottom: -1.2rem; + row-gap: 1.2rem; + border-top: var(--widget-border); + + .widget-value { + display: block; + text-align: center; + background: var(--widget-details-background); - .widget-header { - background: var(--header-background); - margin: -1.2rem -1.2rem 0 -1.2rem; - padding: 0.8rem; - border-radius: 0.1rem 0.1rem 0 0; - position: relative; - min-height: 96px; + &:not(:first-child) { + border-left: var(--widget-border); + } + + &:not(:last-child) { + border-right: var(--widget-border); + } - .header-title { - margin-top: 0; - font-weight: 200; + padding: 0.6rem; - clr-icon { - margin-top: -2px; - } + .widget-title { + margin-bottom: 0.2rem; + margin-top: 0.2rem; + font-size: 1.2rem; + font-weight: 500; + display: block; } - p { - margin-top: 0.3rem; - line-height: 0.8rem; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; + .widget-text { + font-size: 0.55rem; + display: block; + line-height: 0.6rem; } - .header-link { - position: absolute; - top: 0.4rem; - right: 0.3rem; + &.widget-clickable { + cursor: pointer; - clr-icon { - margin-top: -0.1rem; - } + &:hover { + background: var(--widget-clickable-background); + } } - } - .widget-container { - z-index: 100; - - .widget-values { - line-height: 0.8rem; - display: grid; - grid-template: '. . .' 1fr / 1fr 1fr 1fr; - margin-left: -1.2rem; - margin-right: -1.2rem; - margin-bottom: -1.2rem; - row-gap: 1.2rem; - border-top: var(--widget-border); - - .widget-value { - display: block; - text-align: center; - background: var(--widget-details-background); - - &:not(:first-child) { - border-left: var(--widget-border); - } - - &:not(:last-child) { - border-right: var(--widget-border); - } - - padding: 0.6rem; - - .widget-title { - margin-bottom: 0.2rem; - margin-top: 0.2rem; - font-size: 1.2rem; - font-weight: 500; - display: block; - } - - .widget-text { - font-size: 0.55rem; - display: block; - line-height: 0.6rem; - } - - &.widget-clickable { - cursor: pointer; - - &:hover { - background: var(--widget-clickable-background); - } - } - - &.show-details { - background: var(--widget-clickable-background); - - .widget-text { - font-weight: bold; - } - - position: relative; - } - } + &.show-details { + background: var(--widget-clickable-background); + + .widget-text { + font-weight: bold; + } + + position: relative; } + } + } + } + + .widget-details { + z-index: 1; + height: 0; + margin: 1.2rem -1.2rem -1.2rem -1.2rem; + transition: height 0.3s; + overflow: hidden; + background: var(--widget-details-background); + + &.show-details { + height: 240px; + border-top: var(--widget-details-border); } - .widget-details { - z-index: 1; - height: 0; - margin: 1.2rem -1.2rem -1.2rem -1.2rem; - transition: height 0.3s; - overflow: hidden; - background: var(--widget-details-background); + .no-issues { + height: 100%; + width: 100%; + text-align: center; + margin-top: 1.2rem; + + .no-issues-img { + display: block; + height: 120px; + margin: 0 auto; + } + + .no-issues-text { + color: var(--widget-details-color); + } + } - &.show-details { - height: 240px; - border-top: var(--widget-details-border); + .data-details { + height: 240px; + position: relative; + + ::ng-deep clr-datagrid { + height: 100%; + + .datagrid { + margin-top: 0; + flex-basis: 0; + border: 0; + + .datagrid-row { + border-top: none; + border-bottom: none; + } + + .datagrid-placeholder-container { + border-top: none; + } } - .no-issues { - height: 100%; - width: 100%; - text-align: center; - margin-top: 1.2rem; + .datagrid-footer { + padding: 0.1rem 0.5rem; + border-right: none; + border-left: none; + border-bottom: none; + } + } - .no-issues-img { - display: block; - height: 120px; - margin: 0 auto; - } + .data-row { + display: block; + margin-top: 0; + position: relative; + padding-left: 0.2rem; - .no-issues-text { - color: var(--widget-details-color); - } + &.clickable { + cursor: pointer; } - .data-details { - height: 240px; - position: relative; - - ::ng-deep clr-datagrid { - height: 100%; - - .datagrid { - margin-top: 0; - flex-basis: 0; - border: 0; - - .datagrid-row { - border-top: none; - border-bottom: none; - } - - .datagrid-placeholder-container { - border-top: none; - } - } - - .datagrid-footer { - padding: 0.1rem 0.5rem; - border-right: none; - border-left: none; - border-bottom: none; - } + .data-title { + line-height: 0.6rem; + font-weight: 500; + display: block; + margin-top: 0; + position: relative; + + .btn { + margin: 0; + } + + .title-icon { + float: left; + width: 32px; + display: flex; + justify-content: center; + + lib-status-cell { + width: 15px; + display: flex; + justify-content: center; } + } + } - .data-row { - display: block; - margin-top: 0; - position: relative; - padding-left: 0.2rem; - - &.clickable { - cursor: pointer; - } - - .data-title { - line-height: 0.6rem; - font-weight: 500; - display: block; - margin-top: 0; - position: relative; - - .btn { - margin: 0; - } - - .title-icon { - float: left; - width: 32px; - display: flex; - justify-content: center; - - lib-status-cell { - width: 15px; - display: flex; - justify-content: center; - } - } - } - - .data-description { - display: block; - margin-top: 0; - color: var(--widget-details-color); - line-height: 1rem; - - clr-icon { - margin-top: -2px; - } - } - } + .data-description { + display: block; + margin-top: 0; + color: var(--widget-details-color); + line-height: 1rem; + + clr-icon { + margin-top: -2px; + } } + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.spec.ts index 32960b965f..7fa3137e88 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.spec.ts @@ -3,112 +3,122 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; - -import { ErrorHandlerService } from '@versatiledatakit/shared'; - -import { FormatSchedulePipe } from '../../shared/pipes'; - -import { DataJobsApiService } from '../../services'; - -import { DataJobsWidgetOneComponent, WidgetTab } from './data-jobs-widget-one.component'; - -describe('DataJobsWidgetOneComponent', () => { - let errorHandlerServiceStub: jasmine.SpyObj; - - let component: DataJobsWidgetOneComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - // mock service - const dataJobsServiceStub = () => ({ - getJobs: () => - of({ - data: { - totalItems: 1, - totalPages: 11, - content: [ - { - jobName: 'test-job', - item: { - config: { - schedule: { - scheduleCron: '*/5 * * * *' - } - } - } - } - ] - } - }) - }); - errorHandlerServiceStub = jasmine.createSpyObj('errorHandlerService', ['processError', 'handleError']); - - await TestBed.configureTestingModule({ - declarations: [DataJobsWidgetOneComponent, FormatSchedulePipe], - providers: [ - { - provide: DataJobsApiService, - useFactory: dataJobsServiceStub +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { of, throwError } from "rxjs"; + +import { ErrorHandlerService } from "@versatiledatakit/shared"; + +import { FormatSchedulePipe } from "../../shared/pipes"; + +import { DataJobsApiService } from "../../services"; + +import { + DataJobsWidgetOneComponent, + WidgetTab, +} from "./data-jobs-widget-one.component"; + +describe("DataJobsWidgetOneComponent", () => { + let errorHandlerServiceStub: jasmine.SpyObj; + + let component: DataJobsWidgetOneComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + // mock service + const dataJobsServiceStub = () => ({ + getJobs: () => + of({ + data: { + totalItems: 1, + totalPages: 11, + content: [ + { + jobName: "test-job", + item: { + config: { + schedule: { + scheduleCron: "*/5 * * * *", + }, + }, }, - { - provide: ErrorHandlerService, - useValue: errorHandlerServiceStub - } - ] - }).compileComponents(); + }, + ], + }, + }), }); - - beforeEach(() => { - fixture = TestBed.createComponent(DataJobsWidgetOneComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('can load instance', () => { - expect(component).toBeTruthy(); + errorHandlerServiceStub = jasmine.createSpyObj( + "errorHandlerService", + ["processError", "handleError"], + ); + + await TestBed.configureTestingModule({ + declarations: [DataJobsWidgetOneComponent, FormatSchedulePipe], + providers: [ + { + provide: DataJobsApiService, + useFactory: dataJobsServiceStub, + }, + { + provide: ErrorHandlerService, + useValue: errorHandlerServiceStub, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataJobsWidgetOneComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("can load instance", () => { + expect(component).toBeTruthy(); + }); + + describe("ngOnInit", () => { + it("make expected calls", () => { + const dataJobsServiceStub: DataJobsApiService = + fixture.debugElement.injector.get(DataJobsApiService); + spyOn(dataJobsServiceStub, "getJobs").and.callThrough(); + component.ngOnInit(); + expect(dataJobsServiceStub.getJobs).toHaveBeenCalled(); + + // TODO: make expected calls to executions & failures }); + }); - describe('ngOnInit', () => { - it('make expected calls', () => { - const dataJobsServiceStub: DataJobsApiService = fixture.debugElement.injector.get(DataJobsApiService); - spyOn(dataJobsServiceStub, 'getJobs').and.callThrough(); - component.ngOnInit(); - expect(dataJobsServiceStub.getJobs).toHaveBeenCalled(); - - // TODO: make expected calls to executions & failures - }); + describe("switchTab", () => { + it("switch to expected tab", () => { + component.switchTab(WidgetTab.EXECUTIONS); + expect(component.selectedTab).toEqual(WidgetTab.EXECUTIONS); + expect(component.currentPage).toEqual(1); }); - describe('switchTab', () => { - it('switch to expected tab', () => { - component.switchTab(WidgetTab.EXECUTIONS); - expect(component.selectedTab).toEqual(WidgetTab.EXECUTIONS); - expect(component.currentPage).toEqual(1); - }); - - it('collapse panel when click the same selected tab', () => { - component.switchTab(WidgetTab.EXECUTIONS); - component.switchTab(WidgetTab.EXECUTIONS); - expect(component.selectedTab).toEqual(WidgetTab.NONE); - }); + it("collapse panel when click the same selected tab", () => { + component.switchTab(WidgetTab.EXECUTIONS); + component.switchTab(WidgetTab.EXECUTIONS); + expect(component.selectedTab).toEqual(WidgetTab.NONE); }); + }); - describe('handle errors', () => { - it('should catch error when API fails', () => { - expect(component.errorJobs).toEqual(false); - const dataJobsServiceStub: DataJobsApiService = fixture.debugElement.injector.get(DataJobsApiService); - spyOn(dataJobsServiceStub, 'getJobs').and.returnValue(throwError(() => true)); - component.refresh(1, WidgetTab.DATAJOBS); + describe("handle errors", () => { + it("should catch error when API fails", () => { + expect(component.errorJobs).toEqual(false); + const dataJobsServiceStub: DataJobsApiService = + fixture.debugElement.injector.get(DataJobsApiService); + spyOn(dataJobsServiceStub, "getJobs").and.returnValue( + throwError(() => true), + ); + component.refresh(1, WidgetTab.DATAJOBS); - component.jobs$.subscribe(); - component.executions$.subscribe(); - component.failures$.subscribe(); + component.jobs$.subscribe(); + component.executions$.subscribe(); + component.failures$.subscribe(); - expect(component.errorJobs).toEqual(true); + expect(component.errorJobs).toEqual(true); - // TODO: handle errors for executions & failures - }); + // TODO: handle errors for executions & failures }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.ts index 1f1f4c1be3..405914130b 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/data-jobs-widget-one.component.ts @@ -3,189 +3,195 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { Component, HostListener, Input, OnInit } from "@angular/core"; -import { Observable, of, throwError } from 'rxjs'; -import { catchError, delay, map } from 'rxjs/operators'; +import { Observable, of, throwError } from "rxjs"; +import { catchError, delay, map } from "rxjs/operators"; -import { ErrorHandlerService } from '@versatiledatakit/shared'; +import { ErrorHandlerService } from "@versatiledatakit/shared"; -import { ErrorUtil } from '../../shared/utils'; +import { ErrorUtil } from "../../shared/utils"; -import { DataJobExecutionStatus, DataJobPage } from '../../model'; +import { DataJobExecutionStatus, DataJobPage } from "../../model"; -import { DataJobsApiService } from '../../services'; +import { DataJobsApiService } from "../../services"; export enum WidgetTab { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - DATAJOBS, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - EXECUTIONS, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - FAILURES, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - NONE + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + DATAJOBS, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + EXECUTIONS, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + FAILURES, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + NONE, } // TODO: Remove when consume data from API const executionsMock = [ - { - jobName: 'data-job-1', - status: DataJobExecutionStatus.FINISHED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'auserov' - }, - { - jobName: 'data-job-2', - status: DataJobExecutionStatus.FAILED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'buserov' - }, - { - jobName: 'data-job-3', - status: DataJobExecutionStatus.FINISHED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'cuserov' - }, - { - jobName: 'data-job-long-name-test-1', - status: DataJobExecutionStatus.FINISHED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'duserov' - }, - { - jobName: 'data-job-long-name-test-2', - status: DataJobExecutionStatus.FAILED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'euserov' - }, - { - jobName: 'data-job-long-name-test-3', - status: DataJobExecutionStatus.SUBMITTED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'fuserov' - }, - { - jobName: 'data-job-long-name-test-4', - status: DataJobExecutionStatus.PLATFORM_ERROR, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'guserov' - }, - { - jobName: 'data-job-long-name-test-5', - status: DataJobExecutionStatus.USER_ERROR, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'huserov' - }, - { - jobName: 'data-job-a-very-long-name-listed-here-test-1', - status: DataJobExecutionStatus.FINISHED, - startTime: Date.now(), - endTime: Date.now(), - startedBy: 'fuserov' - } + { + jobName: "data-job-1", + status: DataJobExecutionStatus.FINISHED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "auserov", + }, + { + jobName: "data-job-2", + status: DataJobExecutionStatus.FAILED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "buserov", + }, + { + jobName: "data-job-3", + status: DataJobExecutionStatus.FINISHED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "cuserov", + }, + { + jobName: "data-job-long-name-test-1", + status: DataJobExecutionStatus.FINISHED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "duserov", + }, + { + jobName: "data-job-long-name-test-2", + status: DataJobExecutionStatus.FAILED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "euserov", + }, + { + jobName: "data-job-long-name-test-3", + status: DataJobExecutionStatus.SUBMITTED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "fuserov", + }, + { + jobName: "data-job-long-name-test-4", + status: DataJobExecutionStatus.PLATFORM_ERROR, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "guserov", + }, + { + jobName: "data-job-long-name-test-5", + status: DataJobExecutionStatus.USER_ERROR, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "huserov", + }, + { + jobName: "data-job-a-very-long-name-listed-here-test-1", + status: DataJobExecutionStatus.FINISHED, + startTime: Date.now(), + endTime: Date.now(), + startedBy: "fuserov", + }, ]; @Component({ - selector: 'lib-data-jobs-widget-one', - templateUrl: './data-jobs-widget-one.component.html', - styleUrls: ['./data-jobs-widget-one.component.scss', './widget.scss'] + selector: "lib-data-jobs-widget-one", + templateUrl: "./data-jobs-widget-one.component.html", + styleUrls: ["./data-jobs-widget-one.component.scss", "./widget.scss"], }) export class DataJobsWidgetOneComponent implements OnInit { - @Input() manageLink: string; - - selectedTab: WidgetTab = WidgetTab.DATAJOBS; - jobs$: Observable; - /* eslint-disable @typescript-eslint/no-explicit-any */ - executions$: Observable; - failures$: Observable; - /* eslint-enable @typescript-eslint/no-explicit-any */ - widgetTab = WidgetTab; - pageSize = 25; - currentPage = 1; - - errorJobs: boolean; - - constructor( - private readonly dataJobsService: DataJobsApiService, - private readonly errorHandlerService: ErrorHandlerService - ) {} - - @HostListener('window:resize') - onWindowResize() { - // Listener was needed because ChangeDetection cycle doesn't run for Component on "window resize event" - // and doesn't update the data grid column width as expected, - // so only solution was to add dummy listener for window:resize which triggers ChangeDetection cycle inside the Component. - // No-op! Updates the component when the window resizes. This is used for resizing the data grid columns. - } - - ngOnInit() { - this.refreshAll(); - } - - refreshAll() { - this.refresh(this.currentPage, WidgetTab.DATAJOBS); - this.refresh(this.currentPage, WidgetTab.EXECUTIONS); - this.refresh(this.currentPage, WidgetTab.FAILURES); - } - - switchTab(tab: WidgetTab) { - this.selectedTab = this.selectedTab !== tab ? tab : WidgetTab.NONE; - this.currentPage = 1; - } - - refresh(currentPage: number, tab: WidgetTab) { - this.currentPage = currentPage; - - switch (tab) { - case WidgetTab.DATAJOBS: - this.errorJobs = false; - this.jobs$ = this.dataJobsService.getJobs([], '', this.currentPage, this.pageSize).pipe( - map((result) => result?.data), - catchError((error: unknown) => { - this.errorJobs = !!error; - - this.errorHandlerService.processError(ErrorUtil.extractError(error as Error)); - - return throwError(() => error); - }) - ); - break; - case WidgetTab.EXECUTIONS: - // TODO: Consume data from API - this.executions$ = of({ - data: { - totalItems: 0, - totalPages: executionsMock.length / this.pageSize, - content: [] - } - }).pipe( - map((result) => result?.data), - delay(1200) // TODO: Remove delay when consume data from API - ); - break; - case WidgetTab.FAILURES: - // TODO: Consume data from API - const failuresMock = executionsMock.filter((e) => e.status === DataJobExecutionStatus.FAILED); - this.failures$ = of({ - data: { - totalItems: 0, - totalPages: failuresMock.length / this.pageSize, - content: [] - } - }).pipe( - map((result) => result?.data), - delay(1800) // TODO: Remove delay when consume data from API - ); - break; - } + @Input() manageLink: string; + + selectedTab: WidgetTab = WidgetTab.DATAJOBS; + jobs$: Observable; + /* eslint-disable @typescript-eslint/no-explicit-any */ + executions$: Observable; + failures$: Observable; + /* eslint-enable @typescript-eslint/no-explicit-any */ + widgetTab = WidgetTab; + pageSize = 25; + currentPage = 1; + + errorJobs: boolean; + + constructor( + private readonly dataJobsService: DataJobsApiService, + private readonly errorHandlerService: ErrorHandlerService, + ) {} + + @HostListener("window:resize") + onWindowResize() { + // Listener was needed because ChangeDetection cycle doesn't run for Component on "window resize event" + // and doesn't update the data grid column width as expected, + // so only solution was to add dummy listener for window:resize which triggers ChangeDetection cycle inside the Component. + // No-op! Updates the component when the window resizes. This is used for resizing the data grid columns. + } + + ngOnInit() { + this.refreshAll(); + } + + refreshAll() { + this.refresh(this.currentPage, WidgetTab.DATAJOBS); + this.refresh(this.currentPage, WidgetTab.EXECUTIONS); + this.refresh(this.currentPage, WidgetTab.FAILURES); + } + + switchTab(tab: WidgetTab) { + this.selectedTab = this.selectedTab !== tab ? tab : WidgetTab.NONE; + this.currentPage = 1; + } + + refresh(currentPage: number, tab: WidgetTab) { + this.currentPage = currentPage; + + switch (tab) { + case WidgetTab.DATAJOBS: + this.errorJobs = false; + this.jobs$ = this.dataJobsService + .getJobs([], "", this.currentPage, this.pageSize) + .pipe( + map((result) => result?.data), + catchError((error: unknown) => { + this.errorJobs = !!error; + + this.errorHandlerService.processError( + ErrorUtil.extractError(error as Error), + ); + + return throwError(() => error); + }), + ); + break; + case WidgetTab.EXECUTIONS: + // TODO: Consume data from API + this.executions$ = of({ + data: { + totalItems: 0, + totalPages: executionsMock.length / this.pageSize, + content: [], + }, + }).pipe( + map((result) => result?.data), + delay(1200), // TODO: Remove delay when consume data from API + ); + break; + case WidgetTab.FAILURES: + // TODO: Consume data from API + const failuresMock = executionsMock.filter( + (e) => e.status === DataJobExecutionStatus.FAILED, + ); + this.failures$ = of({ + data: { + totalItems: 0, + totalPages: failuresMock.length / this.pageSize, + content: [], + }, + }).pipe( + map((result) => result?.data), + delay(1800), // TODO: Remove delay when consume data from API + ); + break; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/index.ts index c1226f2ade..efd7ea24b2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/index.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-widget-one.component'; -export * from './data-jobs-executions-widget'; -export * from './data-jobs-failed-widget'; -export * from './data-jobs-health-panel'; -export * from './widget-execution-status-gauge'; +export * from "./data-jobs-widget-one.component"; +export * from "./data-jobs-executions-widget"; +export * from "./data-jobs-failed-widget"; +export * from "./data-jobs-health-panel"; +export * from "./widget-execution-status-gauge"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/index.ts index 042bad11c9..e9d17df97e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './widget-execution-status-gauge.component'; +export * from "./widget-execution-status-gauge.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.html index d555f782a7..5301ca18ed 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.html @@ -4,65 +4,63 @@ -->
-
-
- -
Job Executions
-
-
Success Rate
-
- - -

- Success rate is calculated from all executions - from the last 14 days (up to 336 for each job) -

-
-
-
-
-
- {{ successRate | percent }} -
-
- {{ failedExecutions }} failed -
-
- {{ totalExecutions }} total -
-
-
-
- - -
+
+
+ +
Job Executions
+
+
Success Rate
+
+ + +

+ Success rate is calculated from all executions from the last 14 + days (up to 336 for each job) +

+
+
+
+
+ {{ successRate | percent }} +
+
+ {{ failedExecutions }} failed +
+
+ {{ totalExecutions }} total +
+
+
+
+ + +
+
-
- -
+
+ +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.scss index 7a3b9ce1ac..76492a2dce 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.scss @@ -25,27 +25,27 @@ $xxl-min-width: 1919px; $min-el-size: 310px; .medium-and-up { - @media screen and (max-width: $medium-width2) { - display: none !important; - } + @media screen and (max-width: $medium-width2) { + display: none !important; + } } .small-and-up { - @media screen and (max-width: $xs-max) { - display: none !important; - } + @media screen and (max-width: $xs-max) { + display: none !important; + } } .medium-and-down { - @media screen and (min-width: $medium-width) { - display: none !important; - } + @media screen and (min-width: $medium-width) { + display: none !important; + } } .small-and-down { - @media screen and (min-width: $small-width) { - display: none !important; - } + @media screen and (min-width: $small-width) { + display: none !important; + } } $gray1: #747474; @@ -91,503 +91,515 @@ $xs-width: 130px; $red: rgb(194, 84, 0); ::ng-deep .dark lib-widget-execution-status-gauge { - .gauge-container { - margin-top: 50px; - .gauge-meta .value-percent { - //color: $lightblue !important; - } + .gauge-container { + margin-top: 50px; + .gauge-meta .value-percent { + //color: $lightblue !important; + } - svg.ngx-charts g[ngx-charts-pie-arc]:not(.background-arc) > g.arc-group > path.arc { - //fill: $lightblue !important; - } + svg.ngx-charts + g[ngx-charts-pie-arc]:not(.background-arc) + > g.arc-group + > path.arc { + //fill: $lightblue !important; + } - svg.ngx-charts g[ngx-charts-pie-arc].background-arc > g.arc-group > path.arc { - fill: rgba(0, 0, 0, 0.25); - } + svg.ngx-charts + g[ngx-charts-pie-arc].background-arc + > g.arc-group + > path.arc { + fill: rgba(0, 0, 0, 0.25); } + } } ::ng-deep .dark vmw-gauge { - .gauge-container.above-threshold { - .gauge-meta .value-percent { - color: $red !important; - } + .gauge-container.above-threshold { + .gauge-meta .value-percent { + color: $red !important; + } - svg.ngx-charts g[ngx-charts-pie-arc]:not(.background-arc) > g.arc-group > path.arc { - fill: $red !important; - } + svg.ngx-charts + g[ngx-charts-pie-arc]:not(.background-arc) + > g.arc-group + > path.arc { + fill: $red !important; + } - svg.ngx-charts g[ngx-charts-pie-arc].background-arc > g.arc-group > path.arc { - fill: rgba(0, 0, 0, 0.25); - } + svg.ngx-charts + g[ngx-charts-pie-arc].background-arc + > g.arc-group + > path.arc { + fill: rgba(0, 0, 0, 0.25); } + } } ::ng-deep lib-widget-execution-status-gauge { - svg.ngx-charts { - margin: -$chart-margin; + svg.ngx-charts { + margin: -$chart-margin; - g text { - display: none; - } + g text { + display: none; + } - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.1); - opacity: 0.3; + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.1); + opacity: 0.3; - g.background-arc { - display: none; - } - } + g.background-arc { + display: none; } + } } + } - .above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.05); - } - } + .above-threshold svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.05); + } } + } } .gauge-container { - padding: 0; - height: $xl-width - 2 * $chart-margin; - width: $xl-width - 2 * $chart-margin; - position: relative; - margin-top: 50px; - margin-left: auto; - margin-right: auto; - .gauge-chart-container { - width: inherit; - height: inherit; - overflow: hidden; + padding: 0; + height: $xl-width - 2 * $chart-margin; + width: $xl-width - 2 * $chart-margin; + position: relative; + margin-top: 50px; + margin-left: auto; + margin-right: auto; + .gauge-chart-container { + width: inherit; + height: inherit; + overflow: hidden; + } + + .gauge-chart { + height: $xl-width; + width: $xl-width; + margin: 0 auto; + } + + &.above-threshold { + .gauge-meta .value-percent { + color: $red; + } + } + + .gauge-meta { + text-align: center; + position: absolute; + width: inherit; + z-index: 1; + + // integrators can use whatever tag they like for the title, e.g. + // h4 if they want screen readers to notice the gauge as a title, + // or span if they do not. + ::ng-deep .gauge-title { + display: block; + font-size: 19px; + margin-top: 40px; } - .gauge-chart { - height: $xl-width; - width: $xl-width; - margin: 0 auto; + .value-percent { + color: $darkblue; + margin-top: 15px; + margin-bottom: 23px; + font-size: 42.5px; } - &.above-threshold { - .gauge-meta .value-percent { - color: $red; - } + .value-current { + font-size: 16px; } - .gauge-meta { - text-align: center; - position: absolute; - width: inherit; - z-index: 1; - - // integrators can use whatever tag they like for the title, e.g. - // h4 if they want screen readers to notice the gauge as a title, - // or span if they do not. - ::ng-deep .gauge-title { - display: block; - font-size: 19px; - margin-top: 40px; - } + .value-limit { + font-size: 14px; + color: #9a9a9a; + margin-top: 0px; + } + } +} - .value-percent { - color: $darkblue; - margin-top: 15px; - margin-bottom: 23px; - font-size: 42.5px; - } +.large { + &.gauge-container { + height: $lg-width - 2 * $chart-margin + 10px; + width: $lg-width - 2 * $chart-margin; + } - .value-current { - font-size: 16px; - } + .gauge-chart { + height: $lg-width; + width: $lg-width; + } - .value-limit { - font-size: 14px; - color: #9a9a9a; - margin-top: 0px; - } + .gauge-meta { + .value-limit { + margin-top: 0; + } + } + + ::ng-deep &.above-threshold svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.07) !important; + } + } + } + + ::ng-deep svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.12); + } } + } } -.large { +@media (min-width: $large-min) and (max-width: $large-width) { + .auto { &.gauge-container { - height: $lg-width - 2 * $chart-margin + 10px; - width: $lg-width - 2 * $chart-margin; + height: $lg-width - 2 * $chart-margin + 10px; + width: $lg-width - 2 * $chart-margin; } .gauge-chart { - height: $lg-width; - width: $lg-width; + height: $lg-width; + width: $lg-width; } .gauge-meta { - .value-limit { - margin-top: 0; - } + .value-limit { + margin-top: 0; + } } ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.07) !important; - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.07) !important; } + } } ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.12); - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.12); } + } } + } } -@media (min-width: $large-min) and (max-width: $large-width) { - .auto { - &.gauge-container { - height: $lg-width - 2 * $chart-margin + 10px; - width: $lg-width - 2 * $chart-margin; - } +.medium { + &.gauge-container { + height: $med-width - 2 * $chart-margin + 10px; + width: $med-width - 2 * $chart-margin; + } - .gauge-chart { - height: $lg-width; - width: $lg-width; - } + .gauge-chart { + height: $med-width; + width: $med-width; + } - .gauge-meta { - .value-limit { - margin-top: 0; - } - } + .gauge-meta { + ::ng-deep .gauge-title { + font-size: 20px; + margin-top: 28px; + } - ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.07) !important; - } - } - } + .value-percent { + margin: 10px 0; + font-size: 26px; + } - ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.12); - } - } - } + .value-current { + font-size: 16px; + } + + .value-limit { + font-size: 13px; + margin-top: 0; + } + } + + ::ng-deep &.above-threshold svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.09) !important; + } + } + } + + ::ng-deep svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.16); + } } + } } -.medium { +@media (min-width: $medium-min) and (max-width: $medium-width) { + .auto { &.gauge-container { - height: $med-width - 2 * $chart-margin + 10px; - width: $med-width - 2 * $chart-margin; + height: $med-width - 2 * $chart-margin + 10px; + width: $med-width - 2 * $chart-margin; } .gauge-chart { - height: $med-width; - width: $med-width; + height: $med-width; + width: $med-width; } .gauge-meta { - ::ng-deep .gauge-title { - font-size: 20px; - margin-top: 28px; - } + ::ng-deep .gauge-title { + font-size: 20px; + margin-top: 28px; + } - .value-percent { - margin: 10px 0; - font-size: 26px; - } + .value-percent { + margin: 10px 0; + font-size: 26px; + } - .value-current { - font-size: 16px; - } + .value-current { + font-size: 16px; + } - .value-limit { - font-size: 13px; - margin-top: 0; - } + .value-limit { + font-size: 13px; + margin-top: 0; + } } ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.09) !important; - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.09) !important; } + } } ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.16); - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.16); } + } } + } } -@media (min-width: $medium-min) and (max-width: $medium-width) { - .auto { - &.gauge-container { - height: $med-width - 2 * $chart-margin + 10px; - width: $med-width - 2 * $chart-margin; - } +.small { + &.gauge-container { + height: $sm-width - 2 * $chart-margin + 10px; + width: $sm-width - 2 * $chart-margin; + } - .gauge-chart { - height: $med-width; - width: $med-width; - } + .gauge-chart { + height: $sm-width; + width: $sm-width; + } - .gauge-meta { - ::ng-deep .gauge-title { - font-size: 20px; - margin-top: 28px; - } - - .value-percent { - margin: 10px 0; - font-size: 26px; - } - - .value-current { - font-size: 16px; - } - - .value-limit { - font-size: 13px; - margin-top: 0; - } - } + .gauge-meta { + ::ng-deep .gauge-title { + font-size: 17px; + margin-top: 26px; + } - ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.09) !important; - } - } - } + .value-percent { + margin: 0; + font-size: 18.5px; + } - ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.16); - } - } - } + .value-current { + font-size: 14px; + } + + .value-limit { + font-size: 10px; + margin-top: 0; + } + } + + ::ng-deep &.above-threshold svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.1) !important; + } + } + } + + ::ng-deep svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.2); + } } + } } -.small { +@media (min-width: $small-min) and (max-width: $small-max) { + .auto { &.gauge-container { - height: $sm-width - 2 * $chart-margin + 10px; - width: $sm-width - 2 * $chart-margin; + height: $sm-width - 2 * $chart-margin + 10px; + width: $sm-width - 2 * $chart-margin; } .gauge-chart { - height: $sm-width; - width: $sm-width; + height: $sm-width; + width: $sm-width; } .gauge-meta { - ::ng-deep .gauge-title { - font-size: 17px; - margin-top: 26px; - } + ::ng-deep .gauge-title { + font-size: 17px; + margin-top: 26px; + } - .value-percent { - margin: 0; - font-size: 18.5px; - } + .value-percent { + margin: 0; + font-size: 18.5px; + } - .value-current { - font-size: 14px; - } + .value-current { + font-size: 14px; + } - .value-limit { - font-size: 10px; - margin-top: 0; - } + .value-limit { + font-size: 10px; + margin-top: 0; + } } ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.1) !important; - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.1) !important; } + } } ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.2); - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.2); } + } } + } } -@media (min-width: $small-min) and (max-width: $small-max) { - .auto { - &.gauge-container { - height: $sm-width - 2 * $chart-margin + 10px; - width: $sm-width - 2 * $chart-margin; - } +.xs { + &.gauge-container { + height: $xs-width - 2 * $chart-margin + 10px; + width: $xs-width - 2 * $chart-margin; + } - .gauge-chart { - height: $sm-width; - width: $sm-width; - } + .gauge-chart { + height: $xs-width; + width: $xs-width; + } - .gauge-meta { - ::ng-deep .gauge-title { - font-size: 17px; - margin-top: 26px; - } - - .value-percent { - margin: 0; - font-size: 18.5px; - } - - .value-current { - font-size: 14px; - } - - .value-limit { - font-size: 10px; - margin-top: 0; - } - } + .gauge-meta { + .value-current, + .value-limit { + display: none; + } - ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.1) !important; - } - } - } + ::ng-deep .gauge-title { + width: inherit; + font-size: 14px; + margin-top: 14px; + } - ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.2); - } - } - } + .value-percent { + margin-top: 0px; + font-size: 18.5px; } + } + + ::ng-deep &.above-threshold svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.15) !important; + } + } + } + + ::ng-deep svg.ngx-charts { + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.31); + } + } + } } -.xs { +@media (max-width: $xs-max) { + .auto { &.gauge-container { - height: $xs-width - 2 * $chart-margin + 10px; - width: $xs-width - 2 * $chart-margin; + height: $xs-width - 2 * $chart-margin + 10px; + width: $xs-width - 2 * $chart-margin; } .gauge-chart { - height: $xs-width; - width: $xs-width; + height: $xs-width; + width: $xs-width; } .gauge-meta { - .value-current, - .value-limit { - display: none; - } - - ::ng-deep .gauge-title { - width: inherit; - font-size: 14px; - margin-top: 14px; - } - - .value-percent { - margin-top: 0px; - font-size: 18.5px; - } + .value-current, + .value-limit { + display: none; + } + + ::ng-deep .gauge-title { + width: 100%; + font-size: 14px; + margin-top: 14px; + } + + .value-percent { + margin-top: 0px; + font-size: 18.5px; + } } ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.15) !important; - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.15) !important; } + } } ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.31); - } - } - } -} - -@media (max-width: $xs-max) { - .auto { - &.gauge-container { - height: $xs-width - 2 * $chart-margin + 10px; - width: $xs-width - 2 * $chart-margin; - } - - .gauge-chart { - height: $xs-width; - width: $xs-width; - } - - .gauge-meta { - .value-current, - .value-limit { - display: none; - } - - ::ng-deep .gauge-title { - width: 100%; - font-size: 14px; - margin-top: 14px; - } - - .value-percent { - margin-top: 0px; - font-size: 18.5px; - } - } - - ::ng-deep &.above-threshold svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.15) !important; - } - } - } - - ::ng-deep svg.ngx-charts { - g.gauge { - > g:nth-child(2) { - transform: rotate(225deg) scale(1.31); - } - } + g.gauge { + > g:nth-child(2) { + transform: rotate(225deg) scale(1.31); } + } } + } } .centered { - text-align: center; - margin-top: 100px; + text-align: center; + margin-top: 100px; } .success-rate-container { - display: flex; - justify-content: center; - align-items: center; - - .success-rate-info-panel { - font-size: small; - margin-top: 0px; - } + display: flex; + justify-content: center; + align-items: center; + + .success-rate-info-panel { + font-size: small; + margin-top: 0px; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.ts index 9232ed2047..2051f0f731 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget-execution-status-gauge/widget-execution-status-gauge.component.ts @@ -3,46 +3,47 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from "@angular/core"; -import { DataJob } from '../../../model'; +import { DataJob } from "../../../model"; @Component({ - selector: 'lib-widget-execution-status-gauge', - templateUrl: './widget-execution-status-gauge.component.html', - styleUrls: ['./widget-execution-status-gauge.component.scss'] + selector: "lib-widget-execution-status-gauge", + templateUrl: "./widget-execution-status-gauge.component.html", + styleUrls: ["./widget-execution-status-gauge.component.scss"], }) export class WidgetExecutionStatusGaugeComponent implements OnChanges { - @Input() allJobs: DataJob[]; - failedExecutions: number; - successfulExecutions: number; - totalExecutions: number; - successRate: number; - loading = true; + @Input() allJobs: DataJob[]; + failedExecutions: number; + successfulExecutions: number; + totalExecutions: number; + successRate: number; + loading = true; - ngOnChanges(changes: SimpleChanges) { - if (changes['allJobs'].currentValue) { - this.failedExecutions = 0; - this.successfulExecutions = 0; - (changes['allJobs'].currentValue as DataJob[]).forEach((dataJob) => { - if (dataJob.deployments) { - this.failedExecutions += dataJob.deployments[0].failedExecutions; - this.successfulExecutions += dataJob.deployments[0].successfulExecutions; - } - }); - this.totalExecutions = this.failedExecutions + this.successfulExecutions; - this.successRate = this.successfulExecutions / this.totalExecutions; - this.loading = false; + ngOnChanges(changes: SimpleChanges) { + if (changes["allJobs"].currentValue) { + this.failedExecutions = 0; + this.successfulExecutions = 0; + (changes["allJobs"].currentValue as DataJob[]).forEach((dataJob) => { + if (dataJob.deployments) { + this.failedExecutions += dataJob.deployments[0].failedExecutions; + this.successfulExecutions += + dataJob.deployments[0].successfulExecutions; } + }); + this.totalExecutions = this.failedExecutions + this.successfulExecutions; + this.successRate = this.successfulExecutions / this.totalExecutions; + this.loading = false; } + } - customColors(name) { - if (name >= 95) { - return '#5AA220'; - } else if (name >= 90) { - return '#EFC006'; - } else { - return '#F35E44'; - } + customColors(name) { + if (name >= 95) { + return "#5AA220"; + } else if (name >= 90) { + return "#EFC006"; + } else { + return "#F35E44"; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget.scss index 5025b90d64..7a122bf804 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/widgets/widget.scss @@ -3,21 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import 'variables.scss'; +@import "variables.scss"; .widget { - position: relative; - height: 100%; + position: relative; + height: 100%; - .widget-section-container { - padding: 1.2rem; - height: 100%; - border-radius: 0.2rem; - border: none; - box-shadow: - 0 3px 10px 0 rgba(0, 0, 0, 0.1), - 0 3px 5px 0 rgba(0, 0, 0, 0.09); - position: relative; - overflow: hidden; - } + .widget-section-container { + padding: 1.2rem; + height: 100%; + border-radius: 0.2rem; + border: none; + box-shadow: + 0 3px 10px 0 rgba(0, 0, 0, 0.1), + 0 3px 5px 0 rgba(0, 0, 0, 0.09); + position: relative; + overflow: hidden; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/config.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/config.model.ts index 548ba43ed7..0b8d632f50 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/config.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/config.model.ts @@ -5,13 +5,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Type } from '@angular/core'; +import { Type } from "@angular/core"; -import { Observable } from 'rxjs'; +import { Observable } from "rxjs"; -import { DisplayMode } from './grid-config.model'; +import { DisplayMode } from "./grid-config.model"; -export const MISSING_DEFAULT_TEAM_MESSAGE = 'The defaultOwnerTeamName property need to be set for the DATA_PIPELINES_CONFIGS'; +export const MISSING_DEFAULT_TEAM_MESSAGE = + "The defaultOwnerTeamName property need to be set for the DATA_PIPELINES_CONFIGS"; export const RESERVED_DEFAULT_TEAM_NAME_MESSAGE = `The 'default' value is reserved, and can not be used for defaultOwnerTeamName property`; @@ -19,129 +20,129 @@ export const RESERVED_DEFAULT_TEAM_NAME_MESSAGE = `The 'default' value is reserv * ** Configuration map for Data Pipelines library. */ export interface DataPipelinesConfig { - resourceServer?: { - getUrl?: () => string; - }; - - defaultOwnerTeamName: string; - ownerTeamNamesObservable?: Observable; + resourceServer?: { + getUrl?: () => string; + }; + + defaultOwnerTeamName: string; + ownerTeamNamesObservable?: Observable; + /** + * @deprecated + */ + showLogsInsightUrl?: boolean; + + /** + * @deprecated + */ + showExecutionsPage?: boolean; + /** + * ** Flag instruction to show or hide tab for lineage page. + */ + showLineagePage?: boolean; + + /** + * ** Documentation url for Data Pipelines. + */ + dataPipelinesDocumentationUrl?: string; + + // health status url configured by a segment after hostname, including slash with {0} for the id param, + healthStatusUrl?: string; // eg: /dev-center/health-status?dataJob={0} + + /** + * ** Data Job change history configuration. + */ + changeHistory?: { /** - * @deprecated + * ** Url template to external/internal system. */ - showLogsInsightUrl?: boolean; - + urlTemplate: string; /** - * @deprecated + * ** Confirmation title if url template is to external system. */ - showExecutionsPage?: boolean; + confirmationTitle: string; /** - * ** Flag instruction to show or hide tab for lineage page. + * ** Confirmation message component if url template is to external system. */ - showLineagePage?: boolean; - - /** - * ** Documentation url for Data Pipelines. - */ - dataPipelinesDocumentationUrl?: string; - - // health status url configured by a segment after hostname, including slash with {0} for the id param, - healthStatusUrl?: string; // eg: /dev-center/health-status?dataJob={0} - + confirmationMessageComponent: Type; + }; + + /** + * ** Reference to Explore Data Job(s) configuration map. + */ + exploreConfig?: ExploreConfig; + /** + ** Reference to Manage Data Job(s) configuration map. + */ + manageConfig?: ManageConfig; + + /** + * ** Integration providers from Host application. + */ + integrationProviders?: { /** - * ** Data Job change history configuration. + * ** Users related. */ - changeHistory?: { - /** - * ** Url template to external/internal system. - */ - urlTemplate: string; - /** - * ** Confirmation title if url template is to external system. - */ - confirmationTitle: string; - /** - * ** Confirmation message component if url template is to external system. - */ - confirmationMessageComponent: Type; + users?: { + /** + * ** Get logged User email. + */ + getEmail?: () => string; + /** + * ** Get logged User username. + */ + getUsername?: () => string; }; - - /** - * ** Reference to Explore Data Job(s) configuration map. - */ - exploreConfig?: ExploreConfig; - /** - ** Reference to Manage Data Job(s) configuration map. - */ - manageConfig?: ManageConfig; - /** - * ** Integration providers from Host application. + * ** Teams related. */ - integrationProviders?: { - /** - * ** Users related. - */ - users?: { - /** - * ** Get logged User email. - */ - getEmail?: () => string; - /** - * ** Get logged User username. - */ - getUsername?: () => string; - }; - /** - * ** Teams related. - */ - teams?: { - /** - * ** Ensure User membership in early access program identified by its name. - */ - ensureMembershipEarlyAccessProgram?: (key: string) => boolean; - }; + teams?: { + /** + * ** Ensure User membership in early access program identified by its name. + */ + ensureMembershipEarlyAccessProgram?: (key: string) => boolean; }; + }; } /** * ** Configuration map for Explore Data Job(s). */ export interface ExploreConfig { - /** - * ** Shot Teams column in Explore Data Jobs list. - */ - showTeamsColumn?: boolean; - /** - * ** Show Teams section in Explore Data Job details. - */ - showTeamSectionInJobDetails?: boolean; - /** - * ** Show Change history section in Explore Data Job details. - */ - showChangeHistorySectionInJobDetails?: boolean; + /** + * ** Shot Teams column in Explore Data Jobs list. + */ + showTeamsColumn?: boolean; + /** + * ** Show Teams section in Explore Data Job details. + */ + showTeamSectionInJobDetails?: boolean; + /** + * ** Show Change history section in Explore Data Job details. + */ + showChangeHistorySectionInJobDetails?: boolean; } /** * ** Configuration map for Manage Data Job(s). */ export interface ManageConfig { - /** - * ** Shot Teams column in Manage Data Jobs list. - */ - showTeamsColumn?: boolean; - /** - * ** Show Teams section in Manage Data Job details. - */ - showTeamSectionInJobDetails?: boolean; - /** - * ** Show Change history section in Manage Data Job details. - */ - showChangeHistorySectionInJobDetails?: boolean; - selectedTeamNameObservable?: Observable; - filterByTeamName?: boolean; - displayMode?: DisplayMode; - /** - * ** Allow keytab download in Manage Data Job details. - */ - allowKeyTabDownloads?: boolean; + /** + * ** Shot Teams column in Manage Data Jobs list. + */ + showTeamsColumn?: boolean; + /** + * ** Show Teams section in Manage Data Job details. + */ + showTeamSectionInJobDetails?: boolean; + /** + * ** Show Change history section in Manage Data Job details. + */ + showChangeHistorySectionInJobDetails?: boolean; + selectedTeamNameObservable?: Observable; + filterByTeamName?: boolean; + displayMode?: DisplayMode; + /** + * ** Allow keytab download in Manage Data Job details. + */ + allowKeyTabDownloads?: boolean; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/constants.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/constants.model.ts index f8136cdd68..c3b6393562 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/constants.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/constants.model.ts @@ -3,76 +3,78 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { InjectionToken } from '@angular/core'; +import { InjectionToken } from "@angular/core"; -import { DataPipelinesConfig } from './config.model'; +import { DataPipelinesConfig } from "./config.model"; /** * ** Injection Token for Data pipelines config. */ -export const DATA_PIPELINES_CONFIGS = new InjectionToken('DataPipelinesConfig'); +export const DATA_PIPELINES_CONFIGS = new InjectionToken( + "DataPipelinesConfig", +); /** * ** DateTime format pattern provided to Angular DateTime directives/pipes, etc... */ -export const DATA_PIPELINES_DATE_TIME_FORMAT = 'MMM d, y, hh:mm:ss a'; +export const DATA_PIPELINES_DATE_TIME_FORMAT = "MMM d, y, hh:mm:ss a"; /** * ** Team name constant used as key identifier in {@link ComponentState.requestParams}. */ -export const TEAM_NAME_REQ_PARAM = 'team-name-req-param'; +export const TEAM_NAME_REQ_PARAM = "team-name-req-param"; /** * ** Data Job name constant used as key identifier in {@link ComponentState.requestParams}. */ -export const JOB_NAME_REQ_PARAM = 'job-name-req-param'; +export const JOB_NAME_REQ_PARAM = "job-name-req-param"; /** * ** Data Job deployment ID constant used as key identifier in {@link ComponentState.requestParams}. */ -export const JOB_DEPLOYMENT_ID_REQ_PARAM = 'job-deployment-id-req-param'; +export const JOB_DEPLOYMENT_ID_REQ_PARAM = "job-deployment-id-req-param"; /** * ** Data Job status constant used as key identifier in {@link ComponentState.requestParams}. */ -export const JOB_STATUS_REQ_PARAM = 'job-status-req-param'; +export const JOB_STATUS_REQ_PARAM = "job-status-req-param"; /** * ** Filter constant used as key identifier in {@link ComponentState.requestParams}. */ -export const FILTER_REQ_PARAM = 'filter-req-param'; +export const FILTER_REQ_PARAM = "filter-req-param"; /** * ** Order constant used as key identifier in {@link ComponentState.requestParams}. */ -export const ORDER_REQ_PARAM = 'order-req-param'; +export const ORDER_REQ_PARAM = "order-req-param"; /** * ** Data Job details constant used as key identifier in {@link ComponentState.requestParams}. */ -export const JOB_DETAILS_REQ_PARAM = 'job-details-req-param'; +export const JOB_DETAILS_REQ_PARAM = "job-details-req-param"; /** * ** Data Job state constant used as key identifier in {@link ComponentState.requestParams}. */ -export const JOB_STATE_REQ_PARAM = 'job-state-req-param'; +export const JOB_STATE_REQ_PARAM = "job-state-req-param"; /** * ** Data Job state constant used as key identifier in {@link ComponentState.data} */ -export const JOB_STATE_DATA_KEY = 'job-state-data-key'; +export const JOB_STATE_DATA_KEY = "job-state-data-key"; /** * ** Data Jobs states constant used as key identifier in {@link ComponentState.data} */ -export const JOBS_DATA_KEY = 'jobs-data-key'; +export const JOBS_DATA_KEY = "jobs-data-key"; /** * ** Data Job details constant used as key identifier in {@link ComponentState.data} */ -export const JOB_DETAILS_DATA_KEY = 'job-details-data-key'; +export const JOB_DETAILS_DATA_KEY = "job-details-data-key"; /** * ** Data Job Executions constant used as key identifier in {@link ComponentState.data} */ -export const JOB_EXECUTIONS_DATA_KEY = 'job-executions-data-key'; +export const JOB_EXECUTIONS_DATA_KEY = "job-executions-data-key"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-base.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-base.model.ts index 5d3c43ced8..e58ee74cef 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-base.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-base.model.ts @@ -6,93 +6,95 @@ /* eslint-disable @typescript-eslint/naming-convention */ export interface StatusDetails { - enabled: boolean; + enabled: boolean; } export interface GraphQLResponsePage { - content?: T[]; - totalItems?: number; - totalPages?: number; + content?: T[]; + totalItems?: number; + totalPages?: number; } // Deployment -export interface BaseDataJobDeployment extends StatusDetails { - id: string; - contacts?: DataJobContacts; - jobVersion?: string; - deployedDate?: string; - deployedBy?: string; - mode?: string; - resources?: DataJobResources; - schedule?: DataJobSchedule; - vdkVersion?: string; - jobPythonVersion?: string; - status?: DataJobDeploymentStatus; - executions?: E[]; +export interface BaseDataJobDeployment< + E extends DataJobExecution = DataJobExecution, +> extends StatusDetails { + id: string; + contacts?: DataJobContacts; + jobVersion?: string; + deployedDate?: string; + deployedBy?: string; + mode?: string; + resources?: DataJobResources; + schedule?: DataJobSchedule; + vdkVersion?: string; + jobPythonVersion?: string; + status?: DataJobDeploymentStatus; + executions?: E[]; } export enum DataJobDeploymentStatus { - NONE = 'NONE', - SUCCESS = 'SUCCESS', - PLATFORM_ERROR = 'PLATFORM_ERROR', - USER_ERROR = 'USER_ERROR' + NONE = "NONE", + SUCCESS = "SUCCESS", + PLATFORM_ERROR = "PLATFORM_ERROR", + USER_ERROR = "USER_ERROR", } export interface DataJobContacts { - notifiedOnJobFailureUserError: string[]; - notifiedOnJobFailurePlatformError: string[]; - notifiedOnJobSuccess: string[]; - notifiedOnJobDeploy: string[]; + notifiedOnJobFailureUserError: string[]; + notifiedOnJobFailurePlatformError: string[]; + notifiedOnJobSuccess: string[]; + notifiedOnJobDeploy: string[]; } export interface DataJobSchedule { - scheduleCron?: string; - nextRunEpochSeconds?: number; + scheduleCron?: string; + nextRunEpochSeconds?: number; } export interface DataJobResources { - cpuLimit: number; - cpuRequest: number; - memoryLimit: number; - memoryRequest: number; - ephemeralStorageLimit?: number; - ephemeralStorageRequest?: number; - netBandwidthLimit?: number; + cpuLimit: number; + cpuRequest: number; + memoryLimit: number; + memoryRequest: number; + ephemeralStorageLimit?: number; + ephemeralStorageRequest?: number; + netBandwidthLimit?: number; } // Execution export interface DataJobExecution { - id: string; - type?: DataJobExecutionType; - jobName?: string; - status?: DataJobExecutionStatus; - startTime?: string; - endTime?: string; - startedBy?: string; - message?: string; - opId?: string; - logsUrl?: string; - deployment?: BaseDataJobDeployment; + id: string; + type?: DataJobExecutionType; + jobName?: string; + status?: DataJobExecutionStatus; + startTime?: string; + endTime?: string; + startedBy?: string; + message?: string; + opId?: string; + logsUrl?: string; + deployment?: BaseDataJobDeployment; } export enum DataJobExecutionType { - MANUAL = 'MANUAL', - SCHEDULED = 'SCHEDULED' + MANUAL = "MANUAL", + SCHEDULED = "SCHEDULED", } /** * ** Execution Status. */ export enum DataJobExecutionStatus { - SUBMITTED = 'SUBMITTED', - RUNNING = 'RUNNING', - FINISHED = 'FINISHED', // Keep for backward compatibility - SUCCEEDED = 'SUCCEEDED', - CANCELLED = 'CANCELLED', - SKIPPED = 'SKIPPED', - FAILED = 'FAILED', // Keep for backward compatibility - USER_ERROR = 'USER_ERROR', - PLATFORM_ERROR = 'PLATFORM_ERROR' + SUBMITTED = "SUBMITTED", + RUNNING = "RUNNING", + FINISHED = "FINISHED", // Keep for backward compatibility + SUCCEEDED = "SUCCEEDED", + CANCELLED = "CANCELLED", + SKIPPED = "SKIPPED", + FAILED = "FAILED", // Keep for backward compatibility + USER_ERROR = "USER_ERROR", + PLATFORM_ERROR = "PLATFORM_ERROR", } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-deployments.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-deployments.model.ts index ecc4db9d9d..6dadc3a40d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-deployments.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-deployments.model.ts @@ -3,14 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { BaseDataJobDeployment, DataJobExecutionStatus } from './data-job-base.model'; +import { + BaseDataJobDeployment, + DataJobExecutionStatus, +} from "./data-job-base.model"; export interface DataJobDeployment extends BaseDataJobDeployment { - lastDeployedDate?: string; - lastDeployedBy?: string; - lastExecutionStatus?: DataJobExecutionStatus; - lastExecutionDuration?: number; - lastExecutionTime?: string; - successfulExecutions?: number; - failedExecutions?: number; + lastDeployedDate?: string; + lastDeployedBy?: string; + lastExecutionStatus?: DataJobExecutionStatus; + lastExecutionDuration?: number; + lastExecutionTime?: string; + successfulExecutions?: number; + failedExecutions?: number; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-details.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-details.model.ts index 6dc921aaa4..19de25ee7b 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-details.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-details.model.ts @@ -5,9 +5,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { StatusDetails } from './data-job-base.model'; +import { StatusDetails } from "./data-job-base.model"; -import { DataJobExecutionStatusDeprecated } from './data-job-executions.model'; +import { DataJobExecutionStatusDeprecated } from "./data-job-executions.model"; /** * ** Data job details. @@ -15,10 +15,10 @@ import { DataJobExecutionStatusDeprecated } from './data-job-executions.model'; * @deprecated */ export interface DataJobDetails { - job_name?: string; - team?: string; - description?: string; - config?: DataJobConfigDetails; + job_name?: string; + team?: string; + description?: string; + config?: DataJobConfigDetails; } /** @@ -27,8 +27,8 @@ export interface DataJobDetails { * @deprecated */ export interface DataJobConfigDetails { - schedule?: DataJobScheduleDetails; - contacts?: DataJobContactsDetails; + schedule?: DataJobScheduleDetails; + contacts?: DataJobContactsDetails; } /** @@ -37,17 +37,17 @@ export interface DataJobConfigDetails { * @deprecated */ export interface DataJobExecutionDetails { - id: string; - job_name: string; - type: 'manual' | 'scheduled'; - status: DataJobExecutionStatusDeprecated; - start_time: string; - started_by: string; - end_time: string; - op_id: string; - message: string; - logs_url: string; - deployment?: DataJobDeploymentDetails; + id: string; + job_name: string; + type: "manual" | "scheduled"; + status: DataJobExecutionStatusDeprecated; + start_time: string; + started_by: string; + end_time: string; + op_id: string; + message: string; + logs_url: string; + deployment?: DataJobDeploymentDetails; } /** @@ -56,21 +56,21 @@ export interface DataJobExecutionDetails { * @deprecated */ export interface DataJobDeploymentDetails extends StatusDetails { - id: string; - job_version: string; - mode: string; - vdk_version: string; - deployed_by: string; - deployed_date: string; - resources: { - cpu_request: number; - cpu_limit: number; - memory_limit: number; - memory_request: number; - }; - contacts?: DataJobContactsDetails; - schedule?: DataJobScheduleDetails; - python_version: string; + id: string; + job_version: string; + mode: string; + vdk_version: string; + deployed_by: string; + deployed_date: string; + resources: { + cpu_request: number; + cpu_limit: number; + memory_limit: number; + memory_request: number; + }; + contacts?: DataJobContactsDetails; + schedule?: DataJobScheduleDetails; + python_version: string; } /** @@ -79,7 +79,7 @@ export interface DataJobDeploymentDetails extends StatusDetails { * @deprecated */ export interface DataJobScheduleDetails { - schedule_cron: string; + schedule_cron: string; } /** @@ -88,8 +88,8 @@ export interface DataJobScheduleDetails { * @deprecated */ export interface DataJobContactsDetails { - notified_on_job_deploy: string[]; - notified_on_job_failure_platform_error: string[]; - notified_on_job_failure_user_error: string[]; - notified_on_job_success: string[]; + notified_on_job_deploy: string[]; + notified_on_job_failure_platform_error: string[]; + notified_on_job_failure_user_error: string[]; + notified_on_job_success: string[]; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-executions.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-executions.model.ts index ba02ed478c..12f6e5d148 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-executions.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job-executions.model.ts @@ -5,9 +5,13 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { DirectionType } from '@versatiledatakit/shared'; +import { DirectionType } from "@versatiledatakit/shared"; -import { DataJobExecution, DataJobExecutionStatus, GraphQLResponsePage } from './data-job-base.model'; +import { + DataJobExecution, + DataJobExecutionStatus, + GraphQLResponsePage, +} from "./data-job-base.model"; export type DataJobExecutions = DataJobExecution[]; @@ -18,40 +22,40 @@ export type DataJobExecutions = DataJobExecution[]; */ // eslint-disable-next-line no-shadow export enum DataJobExecutionStatusDeprecated { - SUBMITTED = 'submitted', - RUNNING = 'running', - FINISHED = 'finished', // Keep for backward compatibility - SUCCEEDED = 'succeeded', - CANCELLED = 'cancelled', - SKIPPED = 'skipped', - FAILED = 'failed', // Keep for backward compatibility - USER_ERROR = 'user_error', - PLATFORM_ERROR = 'platform_error' + SUBMITTED = "submitted", + RUNNING = "running", + FINISHED = "finished", // Keep for backward compatibility + SUCCEEDED = "succeeded", + CANCELLED = "cancelled", + SKIPPED = "skipped", + FAILED = "failed", // Keep for backward compatibility + USER_ERROR = "user_error", + PLATFORM_ERROR = "platform_error", } /** * ** Request variables fro DataJobs Executions jobsQuery GraphQL API. */ export interface DataJobExecutionsReqVariables { - pageNumber?: number; - pageSize?: number; - filter?: DataJobExecutionFilter; - order?: DataJobExecutionOrder; + pageNumber?: number; + pageSize?: number; + filter?: DataJobExecutionFilter; + order?: DataJobExecutionOrder; } export interface DataJobExecutionFilter { - statusIn?: DataJobExecutionStatus[]; - jobNameIn?: string[]; - teamNameIn?: string[]; - startTimeGte?: string | Date; - endTimeGte?: string | Date; - startTimeLte?: string | Date; - endTimeLte?: string | Date; + statusIn?: DataJobExecutionStatus[]; + jobNameIn?: string[]; + teamNameIn?: string[]; + startTimeGte?: string | Date; + endTimeGte?: string | Date; + startTimeLte?: string | Date; + endTimeLte?: string | Date; } export interface DataJobExecutionOrder { - property: keyof DataJobExecution; - direction: DirectionType; + property: keyof DataJobExecution; + direction: DirectionType; } export type DataJobExecutionsPage = GraphQLResponsePage; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job.model.ts index d20ca0dd20..921642fd6f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/data-job.model.ts @@ -5,47 +5,51 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { ApiPredicate } from '@versatiledatakit/shared'; +import { ApiPredicate } from "@versatiledatakit/shared"; -import { DataJobContacts, DataJobSchedule, GraphQLResponsePage } from './data-job-base.model'; +import { + DataJobContacts, + DataJobSchedule, + GraphQLResponsePage, +} from "./data-job-base.model"; -import { DataJobDeployment } from './data-job-deployments.model'; +import { DataJobDeployment } from "./data-job-deployments.model"; export type DataJobPage = GraphQLResponsePage; export interface DataJob { - jobName?: string; - config?: DataJobConfig; - deployments?: DataJobDeployment[]; + jobName?: string; + config?: DataJobConfig; + deployments?: DataJobDeployment[]; } export interface DataJobConfig { - team?: string; - description?: string; - generateKeytab?: boolean; - sourceUrl?: string; - logsUrl?: string; - schedule?: DataJobSchedule; - contacts?: DataJobContacts; + team?: string; + description?: string; + generateKeytab?: boolean; + sourceUrl?: string; + logsUrl?: string; + schedule?: DataJobSchedule; + contacts?: DataJobContacts; } /** * ** Request variables for DataJobs jobsQuery GraphQL API. */ export interface DataJobReqVariables { - pageNumber?: number; - pageSize?: number; - filter?: ApiPredicate[]; - search?: string; + pageNumber?: number; + pageSize?: number; + filter?: ApiPredicate[]; + search?: string; } export enum DataJobStatus { - ENABLED = 'Enabled', - DISABLED = 'Disabled', - NOT_DEPLOYED = 'Not Deployed' + ENABLED = "Enabled", + DISABLED = "Disabled", + NOT_DEPLOYED = "Not Deployed", } export interface IPcsOAuthDto { - teamName: string; - clientId: string; + teamName: string; + clientId: string; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/grid-config.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/grid-config.model.ts index da5f703238..faf9418dbe 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/grid-config.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/grid-config.model.ts @@ -4,17 +4,17 @@ */ export enum DisplayMode { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - COMPACT = 'compact', - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - STANDARD = 'standard' + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + COMPACT = "compact", + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + STANDARD = "standard", } export interface GridFilters { - jobName?: string; - teamName?: string; - description?: string; - deploymentStatus?: string; - deploymentLastExecutionStatus?: string; - jobPythonVersion?: string; + jobName?: string; + teamName?: string; + description?: string; + deploymentStatus?: string; + deploymentLastExecutionStatus?: string; + jobPythonVersion?: string; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/index.ts index 3e9c253d09..2c22034f03 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/index.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './config.model'; -export * from './constants.model'; -export * from './data-job-base.model'; -export * from './data-job.model'; -export * from './data-job-deployments.model'; -export * from './data-job-details.model'; -export * from './data-job-executions.model'; -export * from './grid-config.model'; -export * from './route.model'; -export * from './toast-definitions.model'; +export * from "./config.model"; +export * from "./constants.model"; +export * from "./data-job-base.model"; +export * from "./data-job.model"; +export * from "./data-job-deployments.model"; +export * from "./data-job-details.model"; +export * from "./data-job-executions.model"; +export * from "./grid-config.model"; +export * from "./route.model"; +export * from "./toast-definitions.model"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/public-api.ts index 1f80deac1a..2c83db2ecf 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/public-api.ts @@ -3,22 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job-base.model'; +export * from "./data-job-base.model"; -export * from './data-job.model'; +export * from "./data-job.model"; -export * from './data-job-details.model'; +export * from "./data-job-details.model"; -export * from './data-job-deployments.model'; +export * from "./data-job-deployments.model"; -export * from './data-job-executions.model'; +export * from "./data-job-executions.model"; -export * from './grid-config.model'; +export * from "./grid-config.model"; -export { ManageConfig, DataPipelinesConfig, ExploreConfig } from './config.model'; +export { + ManageConfig, + DataPipelinesConfig, + ExploreConfig, +} from "./config.model"; -export * from './route.model'; +export * from "./route.model"; -export * from './toast-definitions.model'; +export * from "./toast-definitions.model"; -export { DATA_PIPELINES_CONFIGS } from './constants.model'; +export { DATA_PIPELINES_CONFIGS } from "./constants.model"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/route.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/route.model.ts index 45cbbf99e1..531b523a87 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/route.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/route.model.ts @@ -4,75 +4,76 @@ */ import { - ArrayElement, - TaurusNavigateAction, - TaurusRouteData, - TaurusRouteNavigateBackData, - TaurusRouteNavigateToData, - TaurusRoutes -} from '@versatiledatakit/shared'; + ArrayElement, + TaurusNavigateAction, + TaurusRouteData, + TaurusRouteNavigateBackData, + TaurusRouteNavigateToData, + TaurusRoutes, +} from "@versatiledatakit/shared"; export interface DataPipelinesRestoreUI { - /** - * ** Restore when this condition is met, previous ConfigPath equals to provided. - */ - previousConfigPathLike: string; + /** + * ** Restore when this condition is met, previous ConfigPath equals to provided. + */ + previousConfigPathLike: string; } /** * ** Data pipelines Route data. */ -export interface DataPipelinesRouteData extends TaurusRouteNavigateToData, TaurusRouteNavigateBackData { - /** - * ** Field that has pointer to paramKey for Team in Route config. - */ - teamParamKey?: string; +export interface DataPipelinesRouteData + extends TaurusRouteNavigateToData, TaurusRouteNavigateBackData { + /** + * ** Field that has pointer to paramKey for Team in Route config. + */ + teamParamKey?: string; - /** - * ** Field that has pointer to paramKey for Job in Route config. - */ - jobParamKey?: string; + /** + * ** Field that has pointer to paramKey for Job in Route config. + */ + jobParamKey?: string; - /** - * ** Field flag that enable/disable Listener for Team Change and on Change to do some action. - */ - activateListenerForTeamChange?: boolean; + /** + * ** Field flag that enable/disable Listener for Team Change and on Change to do some action. + */ + activateListenerForTeamChange?: boolean; - /** - * ** Field flag that enable/disable subpage navigation. - * - * - true - enables subpage navigation - * - false - disable subpage navigation and activate default root Page navigation. - */ - activateSubpageNavigation?: boolean; + /** + * ** Field flag that enable/disable subpage navigation. + * + * - true - enables subpage navigation + * - false - disable subpage navigation and activate default root Page navigation. + */ + activateSubpageNavigation?: boolean; - /** - * @inheritDoc - */ - navigateTo?: TaurusNavigateAction; + /** + * @inheritDoc + */ + navigateTo?: TaurusNavigateAction; - /** - * @inheritDoc - */ - navigateBack?: TaurusNavigateAction; + /** + * @inheritDoc + */ + navigateBack?: TaurusNavigateAction; - /** - * ** Field that instruct Component when should restore UI. - */ - restoreUiWhen?: DataPipelinesRestoreUI; + /** + * ** Field that instruct Component when should restore UI. + */ + restoreUiWhen?: DataPipelinesRestoreUI; - /** - * ** Configuring this field, instruct Component on this Route to be in editable mode or no. - * - * - true -> Component is in editable mode. - * - false -> Component is in readonly mode. - */ - editable?: boolean; + /** + * ** Configuring this field, instruct Component on this Route to be in editable mode or no. + * + * - true -> Component is in editable mode. + * - false -> Component is in readonly mode. + */ + editable?: boolean; - /** - * ** Configuring this field, gives context to the Component. - */ - context?: 'manage' | 'explore'; + /** + * ** Configuring this field, gives context to the Component. + */ + context?: "manage" | "explore"; } /** @@ -83,4 +84,6 @@ export type DataPipelinesRoute = ArrayElement; /** * ** Data pipelines Routes configs. */ -export type DataPipelinesRoutes = TaurusRoutes>; +export type DataPipelinesRoutes = TaurusRoutes< + TaurusRouteData +>; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/toast-definitions.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/toast-definitions.model.ts index 7fb738fd19..dd0cbe1755 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/toast-definitions.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/model/toast-definitions.model.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Toast, VmwToastType } from '@versatiledatakit/shared'; +import { Toast, VmwToastType } from "@versatiledatakit/shared"; export class ToastDefinitions { - static successfullyRanJob(jobName: string): Toast { - return { - type: VmwToastType.INFO, - title: `Data job Queued for execution`, - description: `Data job "${jobName}" successfully queued for execution.` - }; - } + static successfullyRanJob(jobName: string): Toast { + return { + type: VmwToastType.INFO, + title: `Data job Queued for execution`, + description: `Data job "${jobName}" successfully queued for execution.`, + }; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.spec.ts index af5c51b64c..f95af603c5 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.spec.ts @@ -3,83 +3,96 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpClient } from '@angular/common/http'; +import { HttpClient } from "@angular/common/http"; -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; -import { of } from 'rxjs'; +import { of } from "rxjs"; -import { ApolloQueryResult, gql, InMemoryCache } from '@apollo/client/core'; +import { ApolloQueryResult, gql, InMemoryCache } from "@apollo/client/core"; -import { Apollo, ApolloBase, QueryRef } from 'apollo-angular'; -import { HttpLink, HttpLinkHandler } from 'apollo-angular/http'; +import { Apollo, ApolloBase, QueryRef } from "apollo-angular"; +import { HttpLink, HttpLinkHandler } from "apollo-angular/http"; -import { ErrorHandlerService } from '@versatiledatakit/shared'; +import { ErrorHandlerService } from "@versatiledatakit/shared"; import { - DATA_PIPELINES_CONFIGS, - DataJobExecutionsPage, - DataJobExecutionsReqVariables, - DataJobPage, - DataJobReqVariables, - DataPipelinesConfig -} from '../model'; + DATA_PIPELINES_CONFIGS, + DataJobExecutionsPage, + DataJobExecutionsReqVariables, + DataJobPage, + DataJobReqVariables, + DataPipelinesConfig, +} from "../model"; -import { DataJobsBaseApiService } from './data-jobs-base.api.service'; +import { DataJobsBaseApiService } from "./data-jobs-base.api.service"; -describe('DataJobsBaseApiService', () => { - let service: DataJobsBaseApiService; - let apolloStub: jasmine.SpyObj; - let httpLinkStub: jasmine.SpyObj; - let httpClientStub: jasmine.SpyObj; - let errorHandlerServiceStub: jasmine.SpyObj; +describe("DataJobsBaseApiService", () => { + let service: DataJobsBaseApiService; + let apolloStub: jasmine.SpyObj; + let httpLinkStub: jasmine.SpyObj; + let httpClientStub: jasmine.SpyObj; + let errorHandlerServiceStub: jasmine.SpyObj; - let apolloBaseStub: jasmine.SpyObj; + let apolloBaseStub: jasmine.SpyObj; - beforeEach(() => { - apolloStub = jasmine.createSpyObj('apolloService', ['use', 'createNamed']); - httpLinkStub = jasmine.createSpyObj('httpLinkService', ['create']); - httpClientStub = jasmine.createSpyObj('httpClientService', ['request']); - errorHandlerServiceStub = jasmine.createSpyObj('errorHandlerService', ['processError', 'handleError']); + beforeEach(() => { + apolloStub = jasmine.createSpyObj("apolloService", [ + "use", + "createNamed", + ]); + httpLinkStub = jasmine.createSpyObj("httpLinkService", [ + "create", + ]); + httpClientStub = jasmine.createSpyObj("httpClientService", [ + "request", + ]); + errorHandlerServiceStub = jasmine.createSpyObj( + "errorHandlerService", + ["processError", "handleError"], + ); - apolloBaseStub = jasmine.createSpyObj('apolloBase', ['query', 'watchQuery']); + apolloBaseStub = jasmine.createSpyObj("apolloBase", [ + "query", + "watchQuery", + ]); - TestBed.configureTestingModule({ - providers: [ - DataJobsBaseApiService, - { provide: Apollo, useValue: apolloStub }, - { provide: HttpLink, useValue: httpLinkStub }, - { provide: HttpClient, useValue: httpClientStub }, - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => - ({ - resourceServer: { - getUrl: () => '' - }, - defaultOwnerTeamName: 'all' - }) as DataPipelinesConfig - }, - { - provide: ErrorHandlerService, - useValue: errorHandlerServiceStub - } - ] - }); - - service = TestBed.inject(DataJobsBaseApiService); + TestBed.configureTestingModule({ + providers: [ + DataJobsBaseApiService, + { provide: Apollo, useValue: apolloStub }, + { provide: HttpLink, useValue: httpLinkStub }, + { provide: HttpClient, useValue: httpClientStub }, + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => + ({ + resourceServer: { + getUrl: () => "", + }, + defaultOwnerTeamName: "all", + }) as DataPipelinesConfig, + }, + { + provide: ErrorHandlerService, + useValue: errorHandlerServiceStub, + }, + ], }); - it('should verify service instance is created', () => { - // Then - expect(service).toBeTruthy(); - }); + service = TestBed.inject(DataJobsBaseApiService); + }); - describe('Methods::', () => { - describe('|getJobs|', () => { - it('should verify will make expected calls', () => { - // Given - const gqlQuery = `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + it("should verify service instance is created", () => { + // Then + expect(service).toBeTruthy(); + }); + + describe("Methods::", () => { + describe("|getJobs|", () => { + it("should verify will make expected calls", () => { + // Given + const gqlQuery = `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -94,66 +107,74 @@ describe('DataJobsBaseApiService', () => { totalItems } }`; - const ownerTeam = 'supercollider_test'; - const dataJobReqVariables: DataJobReqVariables = { - pageNumber: 2, - pageSize: 25, - filter: [], - search: 'sup' - }; - const apolloLinkHandlerStub: HttpLinkHandler = {} as any; - const apolloMockResponse: ApolloQueryResult = { - data: { - content: [], - totalItems: 25, - totalPages: 1 - }, - networkStatus: 7, - loading: false - }; + const ownerTeam = "supercollider_test"; + const dataJobReqVariables: DataJobReqVariables = { + pageNumber: 2, + pageSize: 25, + filter: [], + search: "sup", + }; + const apolloLinkHandlerStub: HttpLinkHandler = {} as any; + const apolloMockResponse: ApolloQueryResult = { + data: { + content: [], + totalItems: 25, + totalPages: 1, + }, + networkStatus: 7, + loading: false, + }; - apolloBaseStub.query.and.returnValue(of(apolloMockResponse)); - apolloStub.use.and.returnValues(null, apolloBaseStub); - httpLinkStub.create.and.returnValue(apolloLinkHandlerStub); + apolloBaseStub.query.and.returnValue(of(apolloMockResponse)); + apolloStub.use.and.returnValues(null, apolloBaseStub); + httpLinkStub.create.and.returnValue(apolloLinkHandlerStub); - // When - let dataJobPage: DataJobPage; - service.getJobs(ownerTeam, gqlQuery, dataJobReqVariables).subscribe((r) => (dataJobPage = r.data)); + // When + let dataJobPage: DataJobPage; + service + .getJobs(ownerTeam, gqlQuery, dataJobReqVariables) + .subscribe((r) => (dataJobPage = r.data)); - // Then - expect(apolloStub.use.calls.argsFor(0)).toEqual([ownerTeam]); - expect(httpLinkStub.create).toHaveBeenCalledWith({ - uri: `/data-jobs/for-team/${ownerTeam}/jobs`, - method: 'GET' - }); - expect(apolloStub.createNamed.calls.argsFor(0)[0]).toEqual(ownerTeam); - expect(apolloStub.createNamed.calls.argsFor(0)[1].cache).toEqual(jasmine.any(InMemoryCache)); - expect(apolloStub.createNamed.calls.argsFor(0)[1].link).toBe(apolloLinkHandlerStub); - expect(apolloStub.createNamed.calls.argsFor(0)[1].defaultOptions).toEqual({ - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - } - }); - expect(apolloStub.use.calls.argsFor(1)).toEqual([ownerTeam]); - expect(apolloBaseStub.query).toHaveBeenCalledWith({ - query: gql` - ${gqlQuery} - `, - variables: dataJobReqVariables - }); - expect(dataJobPage).toBe(apolloMockResponse.data); - }); + // Then + expect(apolloStub.use.calls.argsFor(0)).toEqual([ownerTeam]); + expect(httpLinkStub.create).toHaveBeenCalledWith({ + uri: `/data-jobs/for-team/${ownerTeam}/jobs`, + method: "GET", + }); + expect(apolloStub.createNamed.calls.argsFor(0)[0]).toEqual(ownerTeam); + expect(apolloStub.createNamed.calls.argsFor(0)[1].cache).toEqual( + jasmine.any(InMemoryCache), + ); + expect(apolloStub.createNamed.calls.argsFor(0)[1].link).toBe( + apolloLinkHandlerStub, + ); + expect( + apolloStub.createNamed.calls.argsFor(0)[1].defaultOptions, + ).toEqual({ + watchQuery: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + query: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + }); + expect(apolloStub.use.calls.argsFor(1)).toEqual([ownerTeam]); + expect(apolloBaseStub.query).toHaveBeenCalledWith({ + query: gql` + ${gqlQuery} + `, + variables: dataJobReqVariables, }); + expect(dataJobPage).toBe(apolloMockResponse.data); + }); + }); - describe('|watchForJobs|', () => { - it('should verify will make expected calls', () => { - // Given - const gqlQuery = `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + describe("|watchForJobs|", () => { + it("should verify will make expected calls", () => { + // Given + const gqlQuery = `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -168,70 +189,78 @@ describe('DataJobsBaseApiService', () => { totalItems } }`; - const ownerTeam = 'supercollider_test'; - const dataJobReqVariables: DataJobReqVariables = { - pageNumber: 2, - pageSize: 25, - filter: [], - search: 'sup' - }; - const apolloLinkHandlerStub = {} as HttpLinkHandler; - const apolloQueryResult: ApolloQueryResult = { - data: { - content: [], - totalItems: 25, - totalPages: 1 - }, - networkStatus: 7, - loading: false - }; - const apolloMockResponse: QueryRef = { - valueChanges: of(apolloQueryResult) - } as QueryRef; + const ownerTeam = "supercollider_test"; + const dataJobReqVariables: DataJobReqVariables = { + pageNumber: 2, + pageSize: 25, + filter: [], + search: "sup", + }; + const apolloLinkHandlerStub = {} as HttpLinkHandler; + const apolloQueryResult: ApolloQueryResult = { + data: { + content: [], + totalItems: 25, + totalPages: 1, + }, + networkStatus: 7, + loading: false, + }; + const apolloMockResponse: QueryRef = { + valueChanges: of(apolloQueryResult), + } as QueryRef; - apolloBaseStub.watchQuery.and.returnValue(apolloMockResponse); - apolloStub.use.and.returnValues(null, apolloBaseStub); - httpLinkStub.create.and.returnValue(apolloLinkHandlerStub); + apolloBaseStub.watchQuery.and.returnValue(apolloMockResponse); + apolloStub.use.and.returnValues(null, apolloBaseStub); + httpLinkStub.create.and.returnValue(apolloLinkHandlerStub); - // When - let dataJobPage: DataJobPage; - service.watchForJobs(ownerTeam, gqlQuery, dataJobReqVariables).valueChanges.subscribe((r) => (dataJobPage = r.data)); + // When + let dataJobPage: DataJobPage; + service + .watchForJobs(ownerTeam, gqlQuery, dataJobReqVariables) + .valueChanges.subscribe((r) => (dataJobPage = r.data)); - // Then - expect(apolloStub.use.calls.argsFor(0)).toEqual([ownerTeam]); - expect(httpLinkStub.create).toHaveBeenCalledWith({ - uri: `/data-jobs/for-team/${ownerTeam}/jobs`, - method: 'GET' - }); - expect(apolloStub.createNamed.calls.argsFor(0)[0]).toEqual(ownerTeam); - expect(apolloStub.createNamed.calls.argsFor(0)[1].cache).toEqual(jasmine.any(InMemoryCache)); - expect(apolloStub.createNamed.calls.argsFor(0)[1].link).toBe(apolloLinkHandlerStub); - expect(apolloStub.createNamed.calls.argsFor(0)[1].defaultOptions).toEqual({ - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - } - }); - expect(apolloStub.use.calls.argsFor(1)).toEqual([ownerTeam]); - expect(apolloBaseStub.watchQuery).toHaveBeenCalledWith({ - query: gql` - ${gqlQuery} - `, - variables: dataJobReqVariables - }); - expect(dataJobPage).toBe(apolloQueryResult.data); - }); + // Then + expect(apolloStub.use.calls.argsFor(0)).toEqual([ownerTeam]); + expect(httpLinkStub.create).toHaveBeenCalledWith({ + uri: `/data-jobs/for-team/${ownerTeam}/jobs`, + method: "GET", + }); + expect(apolloStub.createNamed.calls.argsFor(0)[0]).toEqual(ownerTeam); + expect(apolloStub.createNamed.calls.argsFor(0)[1].cache).toEqual( + jasmine.any(InMemoryCache), + ); + expect(apolloStub.createNamed.calls.argsFor(0)[1].link).toBe( + apolloLinkHandlerStub, + ); + expect( + apolloStub.createNamed.calls.argsFor(0)[1].defaultOptions, + ).toEqual({ + watchQuery: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + query: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + }); + expect(apolloStub.use.calls.argsFor(1)).toEqual([ownerTeam]); + expect(apolloBaseStub.watchQuery).toHaveBeenCalledWith({ + query: gql` + ${gqlQuery} + `, + variables: dataJobReqVariables, }); + expect(dataJobPage).toBe(apolloQueryResult.data); + }); + }); - describe('|getExecutions|', () => { - it('should verify will make expected calls', () => { - // Given - // eslint-disable-next-line max-len - const gqlQuery = `query jobsQuery($pageNumber: Int, $pageSize: Int, $filter: DataJobExecutionFilter, $order: DataJobExecutionOrder) + describe("|getExecutions|", () => { + it("should verify will make expected calls", () => { + // Given + // eslint-disable-next-line max-len + const gqlQuery = `query jobsQuery($pageNumber: Int, $pageSize: Int, $filter: DataJobExecutionFilter, $order: DataJobExecutionOrder) { executions(pageNumber: $pageNumber, pageSize: $pageSize, filter: $filter, order: $order) { content { @@ -253,58 +282,66 @@ describe('DataJobsBaseApiService', () => { totalItems } }`; - const ownerTeam = 'supercollider_test'; - const dataJobReqVariables: DataJobExecutionsReqVariables = { - pageNumber: 2, - pageSize: 25 - }; - const apolloLinkHandlerStub = {} as HttpLinkHandler; - const apolloMockResponse: ApolloQueryResult = { - data: { - content: [], - totalItems: 25, - totalPages: 1 - }, - networkStatus: 7, - loading: false - }; + const ownerTeam = "supercollider_test"; + const dataJobReqVariables: DataJobExecutionsReqVariables = { + pageNumber: 2, + pageSize: 25, + }; + const apolloLinkHandlerStub = {} as HttpLinkHandler; + const apolloMockResponse: ApolloQueryResult = { + data: { + content: [], + totalItems: 25, + totalPages: 1, + }, + networkStatus: 7, + loading: false, + }; - apolloBaseStub.query.and.returnValue(of(apolloMockResponse)); - apolloStub.use.and.returnValues(null, apolloBaseStub); - httpLinkStub.create.and.returnValue(apolloLinkHandlerStub); + apolloBaseStub.query.and.returnValue(of(apolloMockResponse)); + apolloStub.use.and.returnValues(null, apolloBaseStub); + httpLinkStub.create.and.returnValue(apolloLinkHandlerStub); - // When - let dataJobExecutionsPage: DataJobExecutionsPage; - service.getExecutions(ownerTeam, gqlQuery, dataJobReqVariables).subscribe((r) => (dataJobExecutionsPage = r.data)); + // When + let dataJobExecutionsPage: DataJobExecutionsPage; + service + .getExecutions(ownerTeam, gqlQuery, dataJobReqVariables) + .subscribe((r) => (dataJobExecutionsPage = r.data)); - // Then - expect(apolloStub.use.calls.argsFor(0)).toEqual([ownerTeam]); - expect(httpLinkStub.create).toHaveBeenCalledWith({ - uri: `/data-jobs/for-team/${ownerTeam}/jobs`, - method: 'GET' - }); - expect(apolloStub.createNamed.calls.argsFor(0)[0]).toEqual(ownerTeam); - expect(apolloStub.createNamed.calls.argsFor(0)[1].cache).toEqual(jasmine.any(InMemoryCache)); - expect(apolloStub.createNamed.calls.argsFor(0)[1].link).toBe(apolloLinkHandlerStub); - expect(apolloStub.createNamed.calls.argsFor(0)[1].defaultOptions).toEqual({ - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - } - }); - expect(apolloStub.use.calls.argsFor(1)).toEqual([ownerTeam]); - expect(apolloBaseStub.query).toHaveBeenCalledWith({ - query: gql` - ${gqlQuery} - `, - variables: dataJobReqVariables - }); - expect(dataJobExecutionsPage).toBe(apolloMockResponse.data); - }); + // Then + expect(apolloStub.use.calls.argsFor(0)).toEqual([ownerTeam]); + expect(httpLinkStub.create).toHaveBeenCalledWith({ + uri: `/data-jobs/for-team/${ownerTeam}/jobs`, + method: "GET", + }); + expect(apolloStub.createNamed.calls.argsFor(0)[0]).toEqual(ownerTeam); + expect(apolloStub.createNamed.calls.argsFor(0)[1].cache).toEqual( + jasmine.any(InMemoryCache), + ); + expect(apolloStub.createNamed.calls.argsFor(0)[1].link).toBe( + apolloLinkHandlerStub, + ); + expect( + apolloStub.createNamed.calls.argsFor(0)[1].defaultOptions, + ).toEqual({ + watchQuery: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + query: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + }); + expect(apolloStub.use.calls.argsFor(1)).toEqual([ownerTeam]); + expect(apolloBaseStub.query).toHaveBeenCalledWith({ + query: gql` + ${gqlQuery} + `, + variables: dataJobReqVariables, }); + expect(dataJobExecutionsPage).toBe(apolloMockResponse.data); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.ts index cf5b39a62f..40d48aed99 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-base.api.service.ts @@ -5,136 +5,152 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Inject, Injectable } from '@angular/core'; +import { Inject, Injectable } from "@angular/core"; -import { Observable } from 'rxjs'; +import { Observable } from "rxjs"; -import { ApolloQueryResult, DefaultOptions, gql, InMemoryCache } from '@apollo/client/core'; +import { + ApolloQueryResult, + DefaultOptions, + gql, + InMemoryCache, +} from "@apollo/client/core"; -import { Apollo, ApolloBase, QueryRef } from 'apollo-angular'; -import { HttpLink } from 'apollo-angular/http'; +import { Apollo, ApolloBase, QueryRef } from "apollo-angular"; +import { HttpLink } from "apollo-angular/http"; -import { TaurusBaseApiService } from '@versatiledatakit/shared'; +import { TaurusBaseApiService } from "@versatiledatakit/shared"; import { - DATA_PIPELINES_CONFIGS, - DataJob, - DataJobExecutionsPage, - DataJobExecutionsReqVariables, - DataJobPage, - DataJobReqVariables, - DataPipelinesConfig -} from '../model'; + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobExecutionsPage, + DataJobExecutionsReqVariables, + DataJobPage, + DataJobReqVariables, + DataPipelinesConfig, +} from "../model"; /** * ** Data Jobs Service build on top of Apollo gql client. */ @Injectable() export class DataJobsBaseApiService extends TaurusBaseApiService { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DataJobsBaseApiService'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Data-Pipelines-Service'; - - private static readonly APOLLO_METHOD = 'GET'; - private static readonly APOLLO_DEFAULT_OPTIONS: DefaultOptions = { - watchQuery: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - }, - query: { - fetchPolicy: 'no-cache', - errorPolicy: 'all' - } - }; - - /** - * ** Constructor. - */ - constructor( - @Inject(DATA_PIPELINES_CONFIGS) private readonly dataPipelinesConfig: DataPipelinesConfig, - private readonly apollo: Apollo, - private readonly httpLink: HttpLink - ) { - super(DataJobsBaseApiService.CLASS_NAME); - - this.registerErrorCodes(DataJobsBaseApiService); + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DataJobsBaseApiService"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Data-Pipelines-Service"; + + private static readonly APOLLO_METHOD = "GET"; + private static readonly APOLLO_DEFAULT_OPTIONS: DefaultOptions = { + watchQuery: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + query: { + fetchPolicy: "no-cache", + errorPolicy: "all", + }, + }; + + /** + * ** Constructor. + */ + constructor( + @Inject(DATA_PIPELINES_CONFIGS) + private readonly dataPipelinesConfig: DataPipelinesConfig, + private readonly apollo: Apollo, + private readonly httpLink: HttpLink, + ) { + super(DataJobsBaseApiService.CLASS_NAME); + + this.registerErrorCodes(DataJobsBaseApiService); + } + + /** + * ** Get all DataJobs for provided OwnerTeam and load data based on provided gqlQuery. + */ + getJobs( + ownerTeam: string, + gqlQuery: string, + variables: DataJobReqVariables, + ): Observable> { + return this.getApolloClientFor(ownerTeam).query({ + query: gql` + ${gqlQuery} + `, + variables, + }); + } + + /** + * ** Create Apollo watcher for gqlQuery. + */ + watchForJobs( + ownerTeam: string, + gqlQuery: string, + variables: DataJobReqVariables, + ): QueryRef { + return this.getApolloClientFor(ownerTeam).watchQuery({ + query: gql` + ${gqlQuery} + `, + variables, + }); + } + + /** + * ** Get all DataJob Executions for provided OwnerTeam and load data based on provided gqlQuery. + */ + getExecutions( + ownerTeam: string, + gqlQuery: string, + variables: DataJobExecutionsReqVariables, + ): Observable> { + return this.getApolloClientFor(ownerTeam).query({ + query: gql` + ${gqlQuery} + `, + variables, + }); + } + + private getApolloClientFor(ownerTeam: string): ApolloBase { + if (!this.apollo.use(ownerTeam)) { + this.apollo.createNamed(ownerTeam, { + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + jobs: (_existing, _options) => { + return {}; + }, + executions: (_existing, _options) => { + return {}; + }, + }, + }, + }, + }), + link: this.httpLink.create({ + uri: `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${ownerTeam}/jobs`, + method: DataJobsBaseApiService.APOLLO_METHOD, + }), + defaultOptions: DataJobsBaseApiService.APOLLO_DEFAULT_OPTIONS, + }); } - /** - * ** Get all DataJobs for provided OwnerTeam and load data based on provided gqlQuery. - */ - getJobs(ownerTeam: string, gqlQuery: string, variables: DataJobReqVariables): Observable> { - return this.getApolloClientFor(ownerTeam).query({ - query: gql` - ${gqlQuery} - `, - variables - }); - } + return this.apollo.use(ownerTeam) as ApolloBase; + } - /** - * ** Create Apollo watcher for gqlQuery. - */ - watchForJobs(ownerTeam: string, gqlQuery: string, variables: DataJobReqVariables): QueryRef { - return this.getApolloClientFor(ownerTeam).watchQuery({ - query: gql` - ${gqlQuery} - `, - variables - }); - } - - /** - * ** Get all DataJob Executions for provided OwnerTeam and load data based on provided gqlQuery. - */ - getExecutions( - ownerTeam: string, - gqlQuery: string, - variables: DataJobExecutionsReqVariables - ): Observable> { - return this.getApolloClientFor(ownerTeam).query({ - query: gql` - ${gqlQuery} - `, - variables - }); - } - - private getApolloClientFor(ownerTeam: string): ApolloBase { - if (!this.apollo.use(ownerTeam)) { - this.apollo.createNamed(ownerTeam, { - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - jobs: (_existing, _options) => { - return {}; - }, - executions: (_existing, _options) => { - return {}; - } - } - } - } - }), - link: this.httpLink.create({ - uri: `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${ownerTeam}/jobs`, - method: DataJobsBaseApiService.APOLLO_METHOD - }), - defaultOptions: DataJobsBaseApiService.APOLLO_DEFAULT_OPTIONS - }); - } - - return this.apollo.use(ownerTeam) as ApolloBase; - } - - private _resolvePipelinesServiceUrl(): string { - return this.dataPipelinesConfig?.resourceServer?.getUrl ? this.dataPipelinesConfig.resourceServer.getUrl() : ''; - } + private _resolvePipelinesServiceUrl(): string { + return this.dataPipelinesConfig?.resourceServer?.getUrl + ? this.dataPipelinesConfig.resourceServer.getUrl() + : ""; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.spec.ts index 1f5df6a106..0cac495f55 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.spec.ts @@ -3,79 +3,91 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { HttpClient } from '@angular/common/http'; - -import { forkJoin, of, Subject } from 'rxjs'; - -import { ApolloQueryResult } from '@apollo/client/core'; - -import { ApiPredicate } from '@versatiledatakit/shared'; - -import { DATA_PIPELINES_CONFIGS, DataJobPage, DataPipelinesConfig } from '../model'; - -import { DataJobsBaseApiService } from './data-jobs-base.api.service'; -import { DataJobsPublicApiService } from './data-jobs-public.api.service'; - -describe('DataJobsPublicApiService', () => { - let dataJobsBaseServiceStub: jasmine.SpyObj; - let httpClientStub: jasmine.SpyObj; - - let service: DataJobsPublicApiService; - - beforeEach(() => { - dataJobsBaseServiceStub = jasmine.createSpyObj('dataJobsBaseServiceStub', ['getJobs']); - dataJobsBaseServiceStub.getJobs.and.returnValue(new Subject>()); - httpClientStub = jasmine.createSpyObj('httpClientService', ['get', 'post']); - - TestBed.configureTestingModule({ - providers: [ - DataJobsPublicApiService, - { provide: HttpClient, useValue: httpClientStub }, - { provide: DataJobsBaseApiService, useValue: dataJobsBaseServiceStub }, - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => - ({ - resourceServer: { - getUrl: () => '' - }, - defaultOwnerTeamName: 'all' - }) as DataPipelinesConfig - } - ] - }); - service = TestBed.inject(DataJobsPublicApiService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); +import { TestBed } from "@angular/core/testing"; +import { HttpClient } from "@angular/common/http"; + +import { forkJoin, of, Subject } from "rxjs"; + +import { ApolloQueryResult } from "@apollo/client/core"; + +import { ApiPredicate } from "@versatiledatakit/shared"; + +import { + DATA_PIPELINES_CONFIGS, + DataJobPage, + DataPipelinesConfig, +} from "../model"; + +import { DataJobsBaseApiService } from "./data-jobs-base.api.service"; +import { DataJobsPublicApiService } from "./data-jobs-public.api.service"; + +describe("DataJobsPublicApiService", () => { + let dataJobsBaseServiceStub: jasmine.SpyObj; + let httpClientStub: jasmine.SpyObj; + + let service: DataJobsPublicApiService; + + beforeEach(() => { + dataJobsBaseServiceStub = jasmine.createSpyObj( + "dataJobsBaseServiceStub", + ["getJobs"], + ); + dataJobsBaseServiceStub.getJobs.and.returnValue( + new Subject>(), + ); + httpClientStub = jasmine.createSpyObj("httpClientService", [ + "get", + "post", + ]); + + TestBed.configureTestingModule({ + providers: [ + DataJobsPublicApiService, + { provide: HttpClient, useValue: httpClientStub }, + { provide: DataJobsBaseApiService, useValue: dataJobsBaseServiceStub }, + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => + ({ + resourceServer: { + getUrl: () => "", + }, + defaultOwnerTeamName: "all", + }) as DataPipelinesConfig, + }, + ], }); - - describe('Methods::', () => { - describe('|getAllDataJobs|', () => { - it('should verify will make expected calls scenario 1', () => { - // Given - const apolloQueryResult: ApolloQueryResult = { - data: { - content: [{}], - totalItems: 1, - totalPages: 1 - }, - loading: false, - networkStatus: 7 - }; - const apolloQueryRef = of(apolloQueryResult); - - dataJobsBaseServiceStub.getJobs.and.returnValue(apolloQueryRef); - - // When - service.getAllDataJobs('teamA').subscribe(); - - // Then - expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( - 'teamA', - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + service = TestBed.inject(DataJobsPublicApiService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + describe("Methods::", () => { + describe("|getAllDataJobs|", () => { + it("should verify will make expected calls scenario 1", () => { + // Given + const apolloQueryResult: ApolloQueryResult = { + data: { + content: [{}], + totalItems: 1, + totalPages: 1, + }, + loading: false, + networkStatus: 7, + }; + const apolloQueryRef = of(apolloQueryResult); + + dataJobsBaseServiceStub.getJobs.and.returnValue(apolloQueryRef); + + // When + service.getAllDataJobs("teamA").subscribe(); + + // Then + expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( + "teamA", + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -90,36 +102,38 @@ describe('DataJobsPublicApiService', () => { totalItems } }`, - { - filter: [], - search: null, - pageNumber: 1, - pageSize: 1000 - } - ); - }); - - it('should verify will make expected calls scenario 2', (done) => { - // Given - const apolloQueryResult: ApolloQueryResult = { - data: { - content: [{}], - totalItems: 2500, - totalPages: 3 - }, - loading: false, - networkStatus: 7 - }; - const apolloQueryRef = of(apolloQueryResult); - let counter = 0; - - dataJobsBaseServiceStub.getJobs.and.returnValue(apolloQueryRef); - - // When/Then - service.getAllDataJobs('teamA').subscribe((value) => { - expect(dataJobsBaseServiceStub.getJobs.calls.argsFor(counter)).toEqual([ - 'teamA', - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + { + filter: [], + search: null, + pageNumber: 1, + pageSize: 1000, + }, + ); + }); + + it("should verify will make expected calls scenario 2", (done) => { + // Given + const apolloQueryResult: ApolloQueryResult = { + data: { + content: [{}], + totalItems: 2500, + totalPages: 3, + }, + loading: false, + networkStatus: 7, + }; + const apolloQueryRef = of(apolloQueryResult); + let counter = 0; + + dataJobsBaseServiceStub.getJobs.and.returnValue(apolloQueryRef); + + // When/Then + service.getAllDataJobs("teamA").subscribe((value) => { + expect( + dataJobsBaseServiceStub.getJobs.calls.argsFor(counter), + ).toEqual([ + "teamA", + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -134,76 +148,76 @@ describe('DataJobsPublicApiService', () => { totalItems } }`, - { - filter: [], - search: null, - pageNumber: counter + 1, - pageSize: 1000 - } - ]); - - counter++; - - expect(value?.length).toEqual(counter); - - if (counter === 3) { - done(); - } - }); - }); + { + filter: [], + search: null, + pageNumber: counter + 1, + pageSize: 1000, + }, + ]); + + counter++; + + expect(value?.length).toEqual(counter); + + if (counter === 3) { + done(); + } }); + }); + }); - describe('|getDataJobsTotalForTeam|', () => { - it('should verify will make expected calls', (done) => { - // Given - const apolloQueryResult: ApolloQueryResult = { - data: { - content: [{}, {}, {}, {}, {}], - totalItems: 5, - totalPages: 1 - }, - loading: false, - networkStatus: 7 - }; - const assertionFilters: ApiPredicate[] = [ - { - property: 'config.team', - pattern: 'teamA', - sort: null - } - ]; - - dataJobsBaseServiceStub.getJobs.and.returnValues( - of(apolloQueryResult), - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - of(undefined), - of({ - ...apolloQueryResult, - data: undefined - }), - of({ - ...apolloQueryResult, - data: { - ...apolloQueryResult.data, - totalItems: undefined - } - }) - ); - - // When/Then - forkJoin([ - service.getDataJobsTotal('teamA'), - service.getDataJobsTotal('teamA'), - service.getDataJobsTotal('teamA'), - service.getDataJobsTotal('teamA') - ]).subscribe(([value1, value2, value3, value4]) => { - expect(value1).toEqual(5); - expect(value2).toEqual(0); - expect(value3).toEqual(0); - expect(value4).toEqual(0); - expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( - 'teamA', - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + describe("|getDataJobsTotalForTeam|", () => { + it("should verify will make expected calls", (done) => { + // Given + const apolloQueryResult: ApolloQueryResult = { + data: { + content: [{}, {}, {}, {}, {}], + totalItems: 5, + totalPages: 1, + }, + loading: false, + networkStatus: 7, + }; + const assertionFilters: ApiPredicate[] = [ + { + property: "config.team", + pattern: "teamA", + sort: null, + }, + ]; + + dataJobsBaseServiceStub.getJobs.and.returnValues( + of(apolloQueryResult), + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + of(undefined), + of({ + ...apolloQueryResult, + data: undefined, + }), + of({ + ...apolloQueryResult, + data: { + ...apolloQueryResult.data, + totalItems: undefined, + }, + }), + ); + + // When/Then + forkJoin([ + service.getDataJobsTotal("teamA"), + service.getDataJobsTotal("teamA"), + service.getDataJobsTotal("teamA"), + service.getDataJobsTotal("teamA"), + ]).subscribe(([value1, value2, value3, value4]) => { + expect(value1).toEqual(5); + expect(value2).toEqual(0); + expect(value3).toEqual(0); + expect(value4).toEqual(0); + expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( + "teamA", + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -216,17 +230,17 @@ describe('DataJobsPublicApiService', () => { totalItems } }`, - { - filter: assertionFilters, - search: null, - pageNumber: 1, - pageSize: 1 - } - ); - - done(); - }); - }); + { + filter: assertionFilters, + search: null, + pageNumber: 1, + pageSize: 1, + }, + ); + + done(); }); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.ts index 82d163fe53..64096d053c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs-public.api.service.ts @@ -3,95 +3,104 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Inject, Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from "@angular/core"; +import { HttpClient } from "@angular/common/http"; -import { EMPTY, expand, Observable, throwError } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; +import { EMPTY, expand, Observable, throwError } from "rxjs"; +import { catchError, map } from "rxjs/operators"; -import { ApiPredicate, TaurusBaseApiService } from '@versatiledatakit/shared'; +import { ApiPredicate, TaurusBaseApiService } from "@versatiledatakit/shared"; -import { ErrorUtil } from '../shared/utils'; +import { ErrorUtil } from "../shared/utils"; -import { DATA_PIPELINES_CONFIGS, DataJob, DataJobPage, DataPipelinesConfig, IPcsOAuthDto } from '../model'; +import { + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobPage, + DataPipelinesConfig, + IPcsOAuthDto, +} from "../model"; -import { DataJobsBaseApiService } from './data-jobs-base.api.service'; +import { DataJobsBaseApiService } from "./data-jobs-base.api.service"; @Injectable() export class DataJobsPublicApiService extends TaurusBaseApiService { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DataJobsPublicApiService'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Data-Pipelines-Service'; - - /** - * ** Constructor. - */ - constructor( - @Inject(DATA_PIPELINES_CONFIGS) private readonly dataPipelinesConfig: DataPipelinesConfig, - private readonly dataJobsBaseService: DataJobsBaseApiService, - private readonly httpClient: HttpClient - ) { - super(DataJobsPublicApiService.CLASS_NAME); - - this.registerErrorCodes(DataJobsPublicApiService); - } - - /** - * ** Retrieve all DataJobs for Team. - */ - getAllDataJobs(team: string): Observable< - Array<{ - jobName?: string; - config?: { - team?: string; - description?: string; - sourceUrl?: string; - }; - }> - > { - const pageSize = 1000; - let pageNumber = 1; - let dataJobs: DataJob[] = []; - - return this._getDataJobsPage(team, pageNumber, pageSize).pipe( - expand((dataJobPage) => { - if (dataJobPage.totalPages <= pageNumber) { - return EMPTY; - } else { - return this._getDataJobsPage(team, ++pageNumber, pageSize); - } - }), - map((dataJobPage) => { - dataJobs = dataJobs.concat(dataJobPage.content as unknown as DataJob[]); - - return dataJobs; - }), - catchError((error: unknown) => throwError(() => ErrorUtil.extractError(error as Error))) - ); - } - - /** - * ** Get total number of Data Jobs assets for Team. - */ - getDataJobsTotal(team: string): Observable { - const filters: ApiPredicate[] = [ - { - property: 'config.team', - pattern: team, - sort: null - } - ]; - - return this.dataJobsBaseService - .getJobs( - team, - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DataJobsPublicApiService"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Data-Pipelines-Service"; + + /** + * ** Constructor. + */ + constructor( + @Inject(DATA_PIPELINES_CONFIGS) + private readonly dataPipelinesConfig: DataPipelinesConfig, + private readonly dataJobsBaseService: DataJobsBaseApiService, + private readonly httpClient: HttpClient, + ) { + super(DataJobsPublicApiService.CLASS_NAME); + + this.registerErrorCodes(DataJobsPublicApiService); + } + + /** + * ** Retrieve all DataJobs for Team. + */ + getAllDataJobs(team: string): Observable< + Array<{ + jobName?: string; + config?: { + team?: string; + description?: string; + sourceUrl?: string; + }; + }> + > { + const pageSize = 1000; + let pageNumber = 1; + let dataJobs: DataJob[] = []; + + return this._getDataJobsPage(team, pageNumber, pageSize).pipe( + expand((dataJobPage) => { + if (dataJobPage.totalPages <= pageNumber) { + return EMPTY; + } else { + return this._getDataJobsPage(team, ++pageNumber, pageSize); + } + }), + map((dataJobPage) => { + dataJobs = dataJobs.concat(dataJobPage.content as unknown as DataJob[]); + + return dataJobs; + }), + catchError((error: unknown) => + throwError(() => ErrorUtil.extractError(error as Error)), + ), + ); + } + + /** + * ** Get total number of Data Jobs assets for Team. + */ + getDataJobsTotal(team: string): Observable { + const filters: ApiPredicate[] = [ + { + property: "config.team", + pattern: team, + sort: null, + }, + ]; + + return this.dataJobsBaseService + .getJobs( + team, + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -104,52 +113,56 @@ export class DataJobsPublicApiService extends TaurusBaseApiService response?.data?.totalItems ?? 0), - catchError((error: unknown) => throwError(() => ErrorUtil.extractError(error as Error))) - ); - } - - /** - * ** Returns OAuth app client id for given Team name. - */ - getTeamOAuthClientId(teamName: string): Observable { - return this.httpClient.get( - `${this._resolvePipelinesServiceUrl()}/data-jobs/teams/${teamName}/oauth-credentials/client-id` - ); - } - - /** - * ** Returns inventory of found OAuth apps client ids for given Team names. - */ - getInventoryOfTeamsOAuthClientIds(clientIds: string[]): Observable { - return this.httpClient.post( - `${this._resolvePipelinesServiceUrl()}/data-jobs/oauth-credentials/client-ids`, - clientIds - ); - } - - /** - * ** Retrieve the data-jobs page. - */ - private _getDataJobsPage( - team: string, - pageNumber: number, - pageSize: number, - filters: ApiPredicate[] = [], - searchQueryValue: string = null - ): Observable { - return this.dataJobsBaseService - .getJobs( - team, - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + { + filter: filters, + search: null, + pageNumber: 1, + pageSize: 1, + }, + ) + .pipe( + map((response) => response?.data?.totalItems ?? 0), + catchError((error: unknown) => + throwError(() => ErrorUtil.extractError(error as Error)), + ), + ); + } + + /** + * ** Returns OAuth app client id for given Team name. + */ + getTeamOAuthClientId(teamName: string): Observable { + return this.httpClient.get( + `${this._resolvePipelinesServiceUrl()}/data-jobs/teams/${teamName}/oauth-credentials/client-id`, + ); + } + + /** + * ** Returns inventory of found OAuth apps client ids for given Team names. + */ + getInventoryOfTeamsOAuthClientIds( + clientIds: string[], + ): Observable { + return this.httpClient.post( + `${this._resolvePipelinesServiceUrl()}/data-jobs/oauth-credentials/client-ids`, + clientIds, + ); + } + + /** + * ** Retrieve the data-jobs page. + */ + private _getDataJobsPage( + team: string, + pageNumber: number, + pageSize: number, + filters: ApiPredicate[] = [], + searchQueryValue: string = null, + ): Observable { + return this.dataJobsBaseService + .getJobs( + team, + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(filter: $filter, search: $search, pageNumber: $pageNumber, pageSize: $pageSize) { content { @@ -164,17 +177,19 @@ export class DataJobsPublicApiService extends TaurusBaseApiService response.data)); - } - - private _resolvePipelinesServiceUrl(): string { - return this.dataPipelinesConfig?.resourceServer?.getUrl ? this.dataPipelinesConfig.resourceServer.getUrl() : ''; - } + { + filter: filters, + search: searchQueryValue, + pageNumber, + pageSize, + }, + ) + .pipe(map((response) => response.data)); + } + + private _resolvePipelinesServiceUrl(): string { + return this.dataPipelinesConfig?.resourceServer?.getUrl + ? this.dataPipelinesConfig.resourceServer.getUrl() + : ""; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.spec.ts index 6ff584b65e..48f636ef45 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.spec.ts @@ -3,100 +3,109 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { HttpClient } from '@angular/common/http'; +import { TestBed } from "@angular/core/testing"; +import { HttpClient } from "@angular/common/http"; -import { of } from 'rxjs'; +import { of } from "rxjs"; -import { ApolloQueryResult } from '@apollo/client/core'; +import { ApolloQueryResult } from "@apollo/client/core"; import { - DATA_PIPELINES_CONFIGS, - DataJob, - DataJobPage, - DataPipelinesConfig, - MISSING_DEFAULT_TEAM_MESSAGE, - RESERVED_DEFAULT_TEAM_NAME_MESSAGE -} from '../model'; - -import { DataJobsBaseApiService } from './data-jobs-base.api.service'; - -import { DataJobsApiService } from './data-jobs.api.service'; - -describe('DataJobsApiService', () => { - let service: DataJobsApiService; - let dataJobsBaseServiceStub: jasmine.SpyObj; - let httpClientStub: jasmine.SpyObj; - - const TEST_JOB_DETAILS = { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - job_name: 'job001', - team: 'taurus', - description: 'descpription001' - }; - - beforeEach(() => { - dataJobsBaseServiceStub = jasmine.createSpyObj('dataJobsBaseService', ['getJobs']); - httpClientStub = jasmine.createSpyObj('httpClient', ['get', 'post', 'patch', 'put', 'delete']); - - httpClientStub.get.and.returnValue(of({})); - httpClientStub.post.and.returnValue(of({})); - httpClientStub.patch.and.returnValue(of({})); - httpClientStub.put.and.returnValue(of({})); - httpClientStub.delete.and.returnValue(of({})); - - TestBed.configureTestingModule({ - providers: [ - DataJobsApiService, - { provide: HttpClient, useValue: httpClientStub }, - { - provide: DataJobsBaseApiService, - useValue: dataJobsBaseServiceStub - }, - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => - ({ - resourceServer: { - getUrl: () => '' - }, - defaultOwnerTeamName: 'all' - }) as DataPipelinesConfig - } - ] - }); - - service = TestBed.inject(DataJobsApiService); + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobPage, + DataPipelinesConfig, + MISSING_DEFAULT_TEAM_MESSAGE, + RESERVED_DEFAULT_TEAM_NAME_MESSAGE, +} from "../model"; + +import { DataJobsBaseApiService } from "./data-jobs-base.api.service"; + +import { DataJobsApiService } from "./data-jobs.api.service"; + +describe("DataJobsApiService", () => { + let service: DataJobsApiService; + let dataJobsBaseServiceStub: jasmine.SpyObj; + let httpClientStub: jasmine.SpyObj; + + const TEST_JOB_DETAILS = { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + job_name: "job001", + team: "taurus", + description: "descpription001", + }; + + beforeEach(() => { + dataJobsBaseServiceStub = jasmine.createSpyObj( + "dataJobsBaseService", + ["getJobs"], + ); + httpClientStub = jasmine.createSpyObj("httpClient", [ + "get", + "post", + "patch", + "put", + "delete", + ]); + + httpClientStub.get.and.returnValue(of({})); + httpClientStub.post.and.returnValue(of({})); + httpClientStub.patch.and.returnValue(of({})); + httpClientStub.put.and.returnValue(of({})); + httpClientStub.delete.and.returnValue(of({})); + + TestBed.configureTestingModule({ + providers: [ + DataJobsApiService, + { provide: HttpClient, useValue: httpClientStub }, + { + provide: DataJobsBaseApiService, + useValue: dataJobsBaseServiceStub, + }, + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => + ({ + resourceServer: { + getUrl: () => "", + }, + defaultOwnerTeamName: "all", + }) as DataPipelinesConfig, + }, + ], }); - it('can load instance', () => { - expect(service).toBeTruthy(); - }); - - describe('Methods::', () => { - describe('|getJobs|', () => { - it('should verify will make expected calls', () => { - // Given - const apolloQueryResult: ApolloQueryResult = { - data: { - content: [{}], - totalItems: 1, - totalPages: 1 - }, - loading: false, - networkStatus: 7 - }; - const apolloQueryRef = of(apolloQueryResult); - - dataJobsBaseServiceStub.getJobs.and.returnValue(apolloQueryRef); - - // When - service.getJobs([], 'searchQueryValue', 1, 1); - - // Then - expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( - 'all', - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + service = TestBed.inject(DataJobsApiService); + }); + + it("can load instance", () => { + expect(service).toBeTruthy(); + }); + + describe("Methods::", () => { + describe("|getJobs|", () => { + it("should verify will make expected calls", () => { + // Given + const apolloQueryResult: ApolloQueryResult = { + data: { + content: [{}], + totalItems: 1, + totalPages: 1, + }, + loading: false, + networkStatus: 7, + }; + const apolloQueryRef = of(apolloQueryResult); + + dataJobsBaseServiceStub.getJobs.and.returnValue(apolloQueryRef); + + // When + service.getJobs([], "searchQueryValue", 1, 1); + + // Then + expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( + "all", + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(pageNumber: $pageNumber, pageSize: $pageSize, filter: $filter, search: $search) { content { @@ -139,37 +148,37 @@ describe('DataJobsApiService', () => { totalItems } }`, - { - pageNumber: 1, - pageSize: 1, - filter: [], - search: 'searchQueryValue' - } - ); - }); - }); - - describe('|getJobListState|', () => { - it('should verify will make expected calls', () => { - // Given - const apolloQueryResult: ApolloQueryResult = { - data: { - content: [{}], - totalItems: 1, - totalPages: 1 - }, - loading: false, - networkStatus: 7 - }; - dataJobsBaseServiceStub.getJobs.and.returnValue(of(apolloQueryResult)); - - // When - service.getJob('supercollider_demo', 'test-job-taur'); - - // Then - expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( - 'supercollider_demo', - `query jobsQuery($filter: [Predicate]) + { + pageNumber: 1, + pageSize: 1, + filter: [], + search: "searchQueryValue", + }, + ); + }); + }); + + describe("|getJobListState|", () => { + it("should verify will make expected calls", () => { + // Given + const apolloQueryResult: ApolloQueryResult = { + data: { + content: [{}], + totalItems: 1, + totalPages: 1, + }, + loading: false, + networkStatus: 7, + }; + dataJobsBaseServiceStub.getJobs.and.returnValue(of(apolloQueryResult)); + + // When + service.getJob("supercollider_demo", "test-job-taur"); + + // Then + expect(dataJobsBaseServiceStub.getJobs).toHaveBeenCalledWith( + "supercollider_demo", + `query jobsQuery($filter: [Predicate]) { jobs(pageNumber: 1, pageSize: 1, filter: $filter) { content { @@ -207,209 +216,251 @@ describe('DataJobsApiService', () => { totalItems } }`, - { - filter: [ - { - property: 'config.team', - pattern: 'supercollider_demo', - sort: null - }, - { - property: 'jobName', - pattern: 'test-job-taur', - sort: null - } - ] - } - ); - }); - - it('should verify will return expected data for given response', (done) => { - // Given - const dataJob: DataJob = {}; - const apolloQueryResult1: ApolloQueryResult = undefined; - const apolloQueryResult2: ApolloQueryResult = { - data: undefined, - loading: false, - networkStatus: 7 - }; - const apolloQueryResult3: ApolloQueryResult = { - data: { - content: undefined - }, - loading: false, - networkStatus: 7 - }; - const apolloQueryResult4: ApolloQueryResult = { - data: { - content: [] - }, - loading: false, - networkStatus: 7 - }; - const apolloQueryResult5: ApolloQueryResult = { - data: { - content: [dataJob], - totalItems: 1, - totalPages: 1 - }, - loading: false, - networkStatus: 7 - }; - dataJobsBaseServiceStub.getJobs.and.returnValues( - of(apolloQueryResult1), - of(apolloQueryResult2), - of(apolloQueryResult3), - of(apolloQueryResult4), - of(apolloQueryResult5) - ); - - const executeNextCall = (executionId) => { - if (executionId === 4) { - service.getJob('supercollider_demo', 'test-job-taur').subscribe((value) => { - expect(value).toBe(dataJob); - done(); - }); - } else { - service.getJob('supercollider_demo', 'test-job-taur').subscribe((value) => { - expect(value).toBeNull(); - executeNextCall(++executionId); - }); - } - }; - - // When/Then - executeNextCall(0); - }); - }); - - describe('|getJobExecution|', () => { - it('should verify will make expected calls', () => { - // Given - const teamName = 'teamA'; - const jobName = 'jobA'; - const executionId = 'executionA'; - - // When - const response = service.getJobExecution(teamName, jobName, executionId); - - // Then - expect(response).toBeDefined(); - expect(httpClientStub.get).toHaveBeenCalledWith( - `/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}` - ); - }); - }); + { + filter: [ + { + property: "config.team", + pattern: "supercollider_demo", + sort: null, + }, + { + property: "jobName", + pattern: "test-job-taur", + sort: null, + }, + ], + }, + ); + }); + + it("should verify will return expected data for given response", (done) => { + // Given + const dataJob: DataJob = {}; + const apolloQueryResult1: ApolloQueryResult = undefined; + const apolloQueryResult2: ApolloQueryResult = { + data: undefined, + loading: false, + networkStatus: 7, + }; + const apolloQueryResult3: ApolloQueryResult = { + data: { + content: undefined, + }, + loading: false, + networkStatus: 7, + }; + const apolloQueryResult4: ApolloQueryResult = { + data: { + content: [], + }, + loading: false, + networkStatus: 7, + }; + const apolloQueryResult5: ApolloQueryResult = { + data: { + content: [dataJob], + totalItems: 1, + totalPages: 1, + }, + loading: false, + networkStatus: 7, + }; + dataJobsBaseServiceStub.getJobs.and.returnValues( + of(apolloQueryResult1), + of(apolloQueryResult2), + of(apolloQueryResult3), + of(apolloQueryResult4), + of(apolloQueryResult5), + ); + + const executeNextCall = (executionId) => { + if (executionId === 4) { + service + .getJob("supercollider_demo", "test-job-taur") + .subscribe((value) => { + expect(value).toBe(dataJob); + done(); + }); + } else { + service + .getJob("supercollider_demo", "test-job-taur") + .subscribe((value) => { + expect(value).toBeNull(); + executeNextCall(++executionId); + }); + } + }; + + // When/Then + executeNextCall(0); + }); }); - describe('validateModuleConfig', () => { - it('validates dataPipelinesModuleConfig with empty defaultOwnerTeamName', () => { - expect(() => - // @ts-ignore - service._validateModuleConfig({ - defaultOwnerTeamName: '' - }) - ).toThrow(new Error(MISSING_DEFAULT_TEAM_MESSAGE)); - }); - - it('validates dataPipelinesModuleConfig with reserved defaultOwnerTeamName', () => { - expect(() => - // @ts-ignore - service._validateModuleConfig({ - defaultOwnerTeamName: 'default' - }) - ).toThrow(new Error(RESERVED_DEFAULT_TEAM_NAME_MESSAGE)); - }); + describe("|getJobExecution|", () => { + it("should verify will make expected calls", () => { + // Given + const teamName = "teamA"; + const jobName = "jobA"; + const executionId = "executionA"; + + // When + const response = service.getJobExecution( + teamName, + jobName, + executionId, + ); + + // Then + expect(response).toBeDefined(); + expect(httpClientStub.get).toHaveBeenCalledWith( + `/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}`, + ); + }); }); - - describe('getJobDetails', () => { - it('returs observable', () => { - const jobDetailsObservable = service.getJobDetails('team001', 'job001'); - expect(jobDetailsObservable).toBeDefined(); - }); + }); + + describe("validateModuleConfig", () => { + it("validates dataPipelinesModuleConfig with empty defaultOwnerTeamName", () => { + expect(() => + // @ts-ignore + service._validateModuleConfig({ + defaultOwnerTeamName: "", + }), + ).toThrow(new Error(MISSING_DEFAULT_TEAM_MESSAGE)); }); - describe('removeJob', () => { - it('returs observable', () => { - const removeJobObservable = service.removeJob('team001', 'job001'); - expect(removeJobObservable).toBeDefined(); - }); + it("validates dataPipelinesModuleConfig with reserved defaultOwnerTeamName", () => { + expect(() => + // @ts-ignore + service._validateModuleConfig({ + defaultOwnerTeamName: "default", + }), + ).toThrow(new Error(RESERVED_DEFAULT_TEAM_NAME_MESSAGE)); }); + }); - describe('downloadFile', () => { - it('returs observable', () => { - const downloadFileObservable = service.downloadFile('team001', 'job001'); - expect(downloadFileObservable).toBeDefined(); - }); + describe("getJobDetails", () => { + it("returs observable", () => { + const jobDetailsObservable = service.getJobDetails("team001", "job001"); + expect(jobDetailsObservable).toBeDefined(); }); + }); - describe('getJobDeployments', () => { - it('returs observable', () => { - const getJobDeploymentsObservable = service.getJobDeployments('team001', 'job001'); - expect(getJobDeploymentsObservable).toBeDefined(); - }); + describe("removeJob", () => { + it("returs observable", () => { + const removeJobObservable = service.removeJob("team001", "job001"); + expect(removeJobObservable).toBeDefined(); }); + }); - describe('updateDataJobStatus', () => { - it('returs observable', () => { - const updateDataJobStatusObservable = service.updateDataJobStatus('team001', 'job001', 'deploy001', false); - expect(updateDataJobStatusObservable).toBeDefined(); - }); + describe("downloadFile", () => { + it("returs observable", () => { + const downloadFileObservable = service.downloadFile("team001", "job001"); + expect(downloadFileObservable).toBeDefined(); }); - - describe('updateDataJobStatus', () => { - it('returs observable', () => { - const updateDataJobStatusObservable = service.updateDataJobStatus('team001', 'job001', null, false); - expect(updateDataJobStatusObservable).toBeDefined(); - }); + }); + + describe("getJobDeployments", () => { + it("returs observable", () => { + const getJobDeploymentsObservable = service.getJobDeployments( + "team001", + "job001", + ); + expect(getJobDeploymentsObservable).toBeDefined(); }); - - describe('updateDataJob', () => { - it('returs observable', () => { - const updateDataJobStatusObservable = service.updateDataJob('team001', 'job001', TEST_JOB_DETAILS); - expect(updateDataJobStatusObservable).toBeDefined(); - }); + }); + + describe("updateDataJobStatus", () => { + it("returs observable", () => { + const updateDataJobStatusObservable = service.updateDataJobStatus( + "team001", + "job001", + "deploy001", + false, + ); + expect(updateDataJobStatusObservable).toBeDefined(); }); - - describe('getJobExecutions', () => { - it('returs observable', () => { - const jobExecutionsObservable = service.getJobExecutions('team001', 'job001'); - expect(jobExecutionsObservable).toBeDefined(); - }); + }); + + describe("updateDataJobStatus", () => { + it("returs observable", () => { + const updateDataJobStatusObservable = service.updateDataJobStatus( + "team001", + "job001", + null, + false, + ); + expect(updateDataJobStatusObservable).toBeDefined(); }); - - describe('executeDataJob', () => { - it('returns observable', () => { - const teamName = 'team001'; - const jobName = 'job001'; - const deploymentId = 'hhw-dff-fgg-100'; - - const executeDataJobObservable = service.executeDataJob(teamName, jobName, deploymentId); - expect(executeDataJobObservable).toBeDefined(); - expect(httpClientStub.post).toHaveBeenCalledWith( - `/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`, - {} - ); - }); + }); + + describe("updateDataJob", () => { + it("returs observable", () => { + const updateDataJobStatusObservable = service.updateDataJob( + "team001", + "job001", + TEST_JOB_DETAILS, + ); + expect(updateDataJobStatusObservable).toBeDefined(); }); - - describe('cancelDataJob', () => { - it('returns observable', () => { - const teamName = 'team001'; - const jobName = 'job001'; - const deploymentId = 'hhw-dff-fgg-100'; - const executionId = 'hhw-dff-fgg-100'; - - const executeDataJobObservable = service.executeDataJob(teamName, jobName, deploymentId); - expect(executeDataJobObservable).toBeDefined(); - expect(httpClientStub.post).toHaveBeenCalledWith( - `/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`, - {} - ); - - const cancelExecutionDataJobObservable = service.cancelDataJobExecution(teamName, jobName, executionId); - expect(cancelExecutionDataJobObservable).toBeDefined(); - expect(httpClientStub.delete).toHaveBeenCalledWith(`/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}`); - }); + }); + + describe("getJobExecutions", () => { + it("returs observable", () => { + const jobExecutionsObservable = service.getJobExecutions( + "team001", + "job001", + ); + expect(jobExecutionsObservable).toBeDefined(); + }); + }); + + describe("executeDataJob", () => { + it("returns observable", () => { + const teamName = "team001"; + const jobName = "job001"; + const deploymentId = "hhw-dff-fgg-100"; + + const executeDataJobObservable = service.executeDataJob( + teamName, + jobName, + deploymentId, + ); + expect(executeDataJobObservable).toBeDefined(); + expect(httpClientStub.post).toHaveBeenCalledWith( + `/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`, + {}, + ); + }); + }); + + describe("cancelDataJob", () => { + it("returns observable", () => { + const teamName = "team001"; + const jobName = "job001"; + const deploymentId = "hhw-dff-fgg-100"; + const executionId = "hhw-dff-fgg-100"; + + const executeDataJobObservable = service.executeDataJob( + teamName, + jobName, + deploymentId, + ); + expect(executeDataJobObservable).toBeDefined(); + expect(httpClientStub.post).toHaveBeenCalledWith( + `/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`, + {}, + ); + + const cancelExecutionDataJobObservable = service.cancelDataJobExecution( + teamName, + jobName, + executionId, + ); + expect(cancelExecutionDataJobObservable).toBeDefined(); + expect(httpClientStub.delete).toHaveBeenCalledWith( + `/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}`, + ); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.ts index f22bdf3551..7c22f30bea 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.api.service.ts @@ -3,78 +3,85 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Inject, Injectable } from '@angular/core'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Inject, Injectable } from "@angular/core"; +import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; -import { ApolloQueryResult } from '@apollo/client/core'; +import { ApolloQueryResult } from "@apollo/client/core"; -import { ApiPredicate, CollectionsUtil, TaurusBaseApiService } from '@versatiledatakit/shared'; +import { + ApiPredicate, + CollectionsUtil, + TaurusBaseApiService, +} from "@versatiledatakit/shared"; import { - DATA_PIPELINES_CONFIGS, - DataJob, - DataJobDeploymentDetails, - DataJobDetails, - DataJobExecutionDetails, - DataJobExecutionFilter, - DataJobExecutionOrder, - DataJobExecutionsPage, - DataJobPage, - DataPipelinesConfig, - MISSING_DEFAULT_TEAM_MESSAGE, - RESERVED_DEFAULT_TEAM_NAME_MESSAGE -} from '../model'; - -import { DataJobsBaseApiService } from './data-jobs-base.api.service'; + DATA_PIPELINES_CONFIGS, + DataJob, + DataJobDeploymentDetails, + DataJobDetails, + DataJobExecutionDetails, + DataJobExecutionFilter, + DataJobExecutionOrder, + DataJobExecutionsPage, + DataJobPage, + DataPipelinesConfig, + MISSING_DEFAULT_TEAM_MESSAGE, + RESERVED_DEFAULT_TEAM_NAME_MESSAGE, +} from "../model"; + +import { DataJobsBaseApiService } from "./data-jobs-base.api.service"; @Injectable() export class DataJobsApiService extends TaurusBaseApiService { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DataJobsApiService'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Data-Pipelines-Service'; - - ownerTeamName: string; - - constructor( - @Inject(DATA_PIPELINES_CONFIGS) private readonly dataPipelinesConfig: DataPipelinesConfig, - private readonly http: HttpClient, - private readonly dataJobsBaseService: DataJobsBaseApiService - ) { - super(DataJobsApiService.CLASS_NAME); - - this.registerErrorCodes(DataJobsApiService); - - this._validateModuleConfig(this.dataPipelinesConfig); - - this.ownerTeamName = this.dataPipelinesConfig?.defaultOwnerTeamName; - if (this.dataPipelinesConfig?.ownerTeamNamesObservable) { - this.dataPipelinesConfig.ownerTeamNamesObservable.subscribe((result: string[]) => { - if (result?.length) { - //Take the first element from the teams array - this.ownerTeamName = result[0]; - } - }); - } + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DataJobsApiService"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Data-Pipelines-Service"; + + ownerTeamName: string; + + constructor( + @Inject(DATA_PIPELINES_CONFIGS) + private readonly dataPipelinesConfig: DataPipelinesConfig, + private readonly http: HttpClient, + private readonly dataJobsBaseService: DataJobsBaseApiService, + ) { + super(DataJobsApiService.CLASS_NAME); + + this.registerErrorCodes(DataJobsApiService); + + this._validateModuleConfig(this.dataPipelinesConfig); + + this.ownerTeamName = this.dataPipelinesConfig?.defaultOwnerTeamName; + if (this.dataPipelinesConfig?.ownerTeamNamesObservable) { + this.dataPipelinesConfig.ownerTeamNamesObservable.subscribe( + (result: string[]) => { + if (result?.length) { + //Take the first element from the teams array + this.ownerTeamName = result[0]; + } + }, + ); } - - getJobs( - filters: ApiPredicate[], - searchQueryValue: string, - pageNumber: number, - pageSize: number - ): Observable> { - return this.dataJobsBaseService.getJobs( - this.ownerTeamName, - `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) + } + + getJobs( + filters: ApiPredicate[], + searchQueryValue: string, + pageNumber: number, + pageSize: number, + ): Observable> { + return this.dataJobsBaseService.getJobs( + this.ownerTeamName, + `query jobsQuery($filter: [Predicate], $search: String, $pageNumber: Int, $pageSize: Int) { jobs(pageNumber: $pageNumber, pageSize: $pageSize, filter: $filter, search: $search) { content { @@ -117,20 +124,20 @@ export class DataJobsApiService extends TaurusBaseApiService totalItems } }`, - { - pageNumber, - pageSize, - filter: filters, - search: searchQueryValue - } - ); - } - - getJob(teamName: string, jobName: string): Observable { - return this.dataJobsBaseService - .getJobs( - teamName, - `query jobsQuery($filter: [Predicate]) + { + pageNumber, + pageSize, + filter: filters, + search: searchQueryValue, + }, + ); + } + + getJob(teamName: string, jobName: string): Observable { + return this.dataJobsBaseService + .getJobs( + teamName, + `query jobsQuery($filter: [Predicate]) { jobs(pageNumber: 1, pageSize: 1, filter: $filter) { content { @@ -168,78 +175,91 @@ export class DataJobsApiService extends TaurusBaseApiService totalItems } }`, - { - filter: this._createTeamJobNameFilter(teamName, jobName) - } - ) - .pipe( - map((response: ApolloQueryResult) => { - if (!CollectionsUtil.isArray(response?.data?.content) || response.data.content.length === 0) { - return null; - } - - return response.data.content[0]; - }) - ); + { + filter: this._createTeamJobNameFilter(teamName, jobName), + }, + ) + .pipe( + map((response: ApolloQueryResult) => { + if ( + !CollectionsUtil.isArray(response?.data?.content) || + response.data.content.length === 0 + ) { + return null; + } + + return response.data.content[0]; + }), + ); + } + + getJobDetails(teamName: string, jobName: string): Observable { + return this.http.get( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}`, + ); + } + + removeJob(teamName: string, jobName: string): Observable { + return this.http.delete( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}`, + ); + } + + downloadFile(teamName: string, jobName: string): Observable { + const httpHeaders = new HttpHeaders(); + httpHeaders.append("Accept", "application/octet-stream"); + + return this.http.get( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/keytab`, + { + headers: httpHeaders, + responseType: "blob", + }, + ); + } + + getJobExecutions( + teamName: string, + jobName: string, + ): Observable; + getJobExecutions( + teamName: string, + jobName: string, + forceGraphQL: boolean, + filter?: DataJobExecutionFilter, + order?: DataJobExecutionOrder, + pageNumber?: number, + pageSize?: number, + ): Observable; + getJobExecutions( + teamName: string, + jobName: string, + forceGraphQL = false, + filter: DataJobExecutionFilter = null, + order: DataJobExecutionOrder = null, + pageNumber: number = null, + pageSize: number = null, + ): Observable | Observable { + if (!forceGraphQL) { + return this.http.get( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions`, + ); } - getJobDetails(teamName: string, jobName: string): Observable { - return this.http.get(`${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}`); - } + const preparedFilter = { ...(filter ?? {}) }; - removeJob(teamName: string, jobName: string): Observable { - return this.http.delete(`${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}`); + if (jobName.length > 0) { + if (CollectionsUtil.isArray(preparedFilter.jobNameIn)) { + preparedFilter.jobNameIn.push(jobName); + } else { + preparedFilter.jobNameIn = [jobName]; + } } - downloadFile(teamName: string, jobName: string): Observable { - const httpHeaders = new HttpHeaders(); - httpHeaders.append('Accept', 'application/octet-stream'); - - return this.http.get(`${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/keytab`, { - headers: httpHeaders, - responseType: 'blob' - }); - } - - getJobExecutions(teamName: string, jobName: string): Observable; - getJobExecutions( - teamName: string, - jobName: string, - forceGraphQL: boolean, - filter?: DataJobExecutionFilter, - order?: DataJobExecutionOrder, - pageNumber?: number, - pageSize?: number - ): Observable; - getJobExecutions( - teamName: string, - jobName: string, - forceGraphQL = false, - filter: DataJobExecutionFilter = null, - order: DataJobExecutionOrder = null, - pageNumber: number = null, - pageSize: number = null - ): Observable | Observable { - if (!forceGraphQL) { - return this.http.get( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions` - ); - } - - const preparedFilter = { ...(filter ?? {}) }; - - if (jobName.length > 0) { - if (CollectionsUtil.isArray(preparedFilter.jobNameIn)) { - preparedFilter.jobNameIn.push(jobName); - } else { - preparedFilter.jobNameIn = [jobName]; - } - } - - return this.dataJobsBaseService - .getExecutions( - teamName, - `query jobsQuery($pageNumber: Int, $pageSize: Int, $filter: DataJobExecutionFilter, $order: DataJobExecutionOrder) + return this.dataJobsBaseService + .getExecutions( + teamName, + `query jobsQuery($pageNumber: Int, $pageSize: Int, $filter: DataJobExecutionFilter, $order: DataJobExecutionOrder) { executions(pageNumber: $pageNumber, pageSize: $pageSize, filter: $filter, order: $order) { content { @@ -276,85 +296,108 @@ export class DataJobsApiService extends TaurusBaseApiService totalItems } }`, - { - pageNumber: pageNumber ?? 1, - pageSize: pageSize ?? 500, - filter: preparedFilter, - order: order ?? null - } - ) - .pipe(map((response) => response.data)); - } - - getJobExecution(teamName: string, jobName: string, executionId: string): Observable { - return this.http.get( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}` - ); - } - - getJobDeployments(teamName: string, jobName: string): Observable { - return this.http.get( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments` - ); - } - - updateDataJobStatus( - teamName: string, - jobName: string, - deploymentId: string, - dataJobEnabled: boolean - ): Observable<{ enabled: boolean }> { - const deploymentStatus = { enabled: dataJobEnabled }; - - if (!deploymentId) { - console.log(`Status update will be processed with default deploymentId`); - deploymentId = 'default'; - } - - return this.http.patch<{ enabled: boolean }>( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}`, - deploymentStatus - ); - } - - updateDataJob(teamName: string, jobName: string, dataJob: DataJobDetails): Observable { - return this.http.put( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}`, - dataJob - ); + { + pageNumber: pageNumber ?? 1, + pageSize: pageSize ?? 500, + filter: preparedFilter, + order: order ?? null, + }, + ) + .pipe(map((response) => response.data)); + } + + getJobExecution( + teamName: string, + jobName: string, + executionId: string, + ): Observable { + return this.http.get( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}`, + ); + } + + getJobDeployments( + teamName: string, + jobName: string, + ): Observable { + return this.http.get( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments`, + ); + } + + updateDataJobStatus( + teamName: string, + jobName: string, + deploymentId: string, + dataJobEnabled: boolean, + ): Observable<{ enabled: boolean }> { + const deploymentStatus = { enabled: dataJobEnabled }; + + if (!deploymentId) { + console.log(`Status update will be processed with default deploymentId`); + deploymentId = "default"; } - executeDataJob(teamName: string, jobName: string, deploymentId: string): Observable { - return this.http.post( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`, - {} - ); + return this.http.patch<{ enabled: boolean }>( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}`, + deploymentStatus, + ); + } + + updateDataJob( + teamName: string, + jobName: string, + dataJob: DataJobDetails, + ): Observable { + return this.http.put( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}`, + dataJob, + ); + } + + executeDataJob( + teamName: string, + jobName: string, + deploymentId: string, + ): Observable { + return this.http.post( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/deployments/${deploymentId}/executions`, + {}, + ); + } + + cancelDataJobExecution( + teamName: string, + jobName: string, + executionId: string, + ): Observable { + return this.http.delete( + `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}`, + ); + } + + private _resolvePipelinesServiceUrl(): string { + return this.dataPipelinesConfig?.resourceServer?.getUrl + ? this.dataPipelinesConfig.resourceServer.getUrl() + : ""; + } + + private _createTeamJobNameFilter(teamName: string, jobName: string) { + return [ + { property: "config.team", pattern: teamName, sort: null }, + { property: "jobName", pattern: jobName, sort: null }, + ]; + } + + private _validateModuleConfig( + dataPipelinesConfig: DataPipelinesConfig, + ): void { + if (!dataPipelinesConfig?.defaultOwnerTeamName) { + throw new Error(MISSING_DEFAULT_TEAM_MESSAGE); } - cancelDataJobExecution(teamName: string, jobName: string, executionId: string): Observable { - return this.http.delete( - `${this._resolvePipelinesServiceUrl()}/data-jobs/for-team/${teamName}/jobs/${jobName}/executions/${executionId}` - ); - } - - private _resolvePipelinesServiceUrl(): string { - return this.dataPipelinesConfig?.resourceServer?.getUrl ? this.dataPipelinesConfig.resourceServer.getUrl() : ''; - } - - private _createTeamJobNameFilter(teamName: string, jobName: string) { - return [ - { property: 'config.team', pattern: teamName, sort: null }, - { property: 'jobName', pattern: jobName, sort: null } - ]; - } - - private _validateModuleConfig(dataPipelinesConfig: DataPipelinesConfig): void { - if (!dataPipelinesConfig?.defaultOwnerTeamName) { - throw new Error(MISSING_DEFAULT_TEAM_MESSAGE); - } - - if (dataPipelinesConfig?.defaultOwnerTeamName === 'default') { - throw new Error(RESERVED_DEFAULT_TEAM_NAME_MESSAGE); - } + if (dataPipelinesConfig?.defaultOwnerTeamName === "default") { + throw new Error(RESERVED_DEFAULT_TEAM_NAME_MESSAGE); } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.spec.ts index 2f0d9501fb..0f67d184c2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.spec.ts @@ -3,206 +3,255 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; + +import { Observable } from "rxjs"; + +import { + ComponentModel, + ComponentService, + ComponentStateImpl, + RouterState, + RouteState, +} from "@versatiledatakit/shared"; + +import { + FETCH_DATA_JOB, + FETCH_DATA_JOB_EXECUTIONS, + FETCH_DATA_JOBS, + UPDATE_DATA_JOB, +} from "../state/actions"; +import { TASK_UPDATE_JOB_DESCRIPTION } from "../state/tasks"; + +import { DataJobsService, DataJobsServiceImpl } from "./data-jobs.service"; + +describe("DataJobsService -> DataJobsServiceImpl", () => { + let componentServiceStub: jasmine.SpyObj; + + let service: DataJobsService; + + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["load", "dispatchAction"], + ); + + TestBed.configureTestingModule({ + providers: [ + { provide: ComponentService, useValue: componentServiceStub }, + { provide: DataJobsService, useClass: DataJobsServiceImpl }, + ], + }); + + service = TestBed.inject(DataJobsService); + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + }); + + describe("Methods::", () => { + describe("|loadJob|", () => { + it("should verify will invoke expected method", () => { + // Given + const model = ComponentModel.of( + ComponentStateImpl.of({ + id: "test-component", + }), + RouterState.of(RouteState.empty(), 1), + ); + + // When + service.loadJob(model); + + // Then + expect(componentServiceStub.load).toHaveBeenCalledWith( + model.getComponentState(), + ); + expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith( + FETCH_DATA_JOB, + model.getComponentState(), + ); + }); + }); + + describe("|loadJobs|", () => { + it("should verify will invoke expected method", () => { + // Given + const model = ComponentModel.of( + ComponentStateImpl.of({ + id: "test-component", + }), + RouterState.of(RouteState.empty(), 1), + ); + + // When + service.loadJobs(model); + + // Then + expect(componentServiceStub.load).toHaveBeenCalledWith( + model.getComponentState(), + ); + expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith( + FETCH_DATA_JOBS, + model.getComponentState(), + ); + }); + }); + + describe("|loadJobExecutions|", () => { + it("should verify will invoke expected method", () => { + // Given + const model = ComponentModel.of( + ComponentStateImpl.of({ + id: "test-component", + }), + RouterState.of(RouteState.empty(), 1), + ); + + // When + service.loadJobExecutions(model); + + // Then + expect(componentServiceStub.load).toHaveBeenCalledWith( + model.getComponentState(), + ); + expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith( + FETCH_DATA_JOB_EXECUTIONS, + model.getComponentState(), + ); + }); + }); + + describe("|updateJob|", () => { + it("should verify will invoke expected method", () => { + // Given + const model = ComponentModel.of( + ComponentStateImpl.of({ + id: "test-component", + }), + RouterState.of(RouteState.empty(), 1), + ); -import { Observable } from 'rxjs'; + // When + service.updateJob(model, TASK_UPDATE_JOB_DESCRIPTION); -import { ComponentModel, ComponentService, ComponentStateImpl, RouterState, RouteState } from '@versatiledatakit/shared'; + // Then + expect(componentServiceStub.load).toHaveBeenCalledWith( + model.getComponentState(), + ); + expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith( + UPDATE_DATA_JOB, + model.getComponentState(), + TASK_UPDATE_JOB_DESCRIPTION, + ); + }); + }); + + describe("|getNotifiedForRunningJobExecutionId|", () => { + it("should verify will return Observable", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const asObservableSpy = spyOn( + (service as DataJobsServiceImpl)["_runningJobExecutionId"], + "asObservable", + ).and.callThrough(); -import { FETCH_DATA_JOB, FETCH_DATA_JOB_EXECUTIONS, FETCH_DATA_JOBS, UPDATE_DATA_JOB } from '../state/actions'; -import { TASK_UPDATE_JOB_DESCRIPTION } from '../state/tasks'; + // When + const observable = service.getNotifiedForRunningJobExecutionId(); + + // Then + expect(observable).toBeInstanceOf(Observable); + expect(asObservableSpy).toHaveBeenCalled(); + }); + }); -import { DataJobsService, DataJobsServiceImpl } from './data-jobs.service'; + describe("|notifyForRunningJobExecutionId|", () => { + it("should verify will notify correct Subject", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const nextSpy = spyOn( + (service as DataJobsServiceImpl)["_runningJobExecutionId"], + "next", + ).and.callThrough(); -describe('DataJobsService -> DataJobsServiceImpl', () => { - let componentServiceStub: jasmine.SpyObj; + // When + service.notifyForRunningJobExecutionId("xy"); - let service: DataJobsService; + // Then + expect(nextSpy).toHaveBeenCalledWith("xy"); + }); + }); - beforeEach(() => { - componentServiceStub = jasmine.createSpyObj('componentService', ['load', 'dispatchAction']); + describe("|getNotifiedForJobExecutions|", () => { + it("should verify will return Observable", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const asObservableSpy = spyOn( + (service as DataJobsServiceImpl)["_jobExecutions"], + "asObservable", + ).and.callThrough(); - TestBed.configureTestingModule({ - providers: [ - { provide: ComponentService, useValue: componentServiceStub }, - { provide: DataJobsService, useClass: DataJobsServiceImpl } - ] - }); + // When + const observable = service.getNotifiedForJobExecutions(); - service = TestBed.inject(DataJobsService); + // Then + expect(observable).toBeInstanceOf(Observable); + expect(asObservableSpy).toHaveBeenCalled(); + }); }); - it('should verify instance is created', () => { + describe("|notifyForJobExecutions|", () => { + it("should verify will notify correct Subject", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const nextSpy = spyOn( + (service as DataJobsServiceImpl)["_jobExecutions"], + "next", + ).and.callThrough(); + + // When + service.notifyForJobExecutions([null]); + // Then - expect(service).toBeDefined(); + expect(nextSpy).toHaveBeenCalledWith([null]); + }); }); - describe('Methods::', () => { - describe('|loadJob|', () => { - it('should verify will invoke expected method', () => { - // Given - const model = ComponentModel.of( - ComponentStateImpl.of({ - id: 'test-component' - }), - RouterState.of(RouteState.empty(), 1) - ); - - // When - service.loadJob(model); - - // Then - expect(componentServiceStub.load).toHaveBeenCalledWith(model.getComponentState()); - expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith(FETCH_DATA_JOB, model.getComponentState()); - }); - }); - - describe('|loadJobs|', () => { - it('should verify will invoke expected method', () => { - // Given - const model = ComponentModel.of( - ComponentStateImpl.of({ - id: 'test-component' - }), - RouterState.of(RouteState.empty(), 1) - ); - - // When - service.loadJobs(model); - - // Then - expect(componentServiceStub.load).toHaveBeenCalledWith(model.getComponentState()); - expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith(FETCH_DATA_JOBS, model.getComponentState()); - }); - }); - - describe('|loadJobExecutions|', () => { - it('should verify will invoke expected method', () => { - // Given - const model = ComponentModel.of( - ComponentStateImpl.of({ - id: 'test-component' - }), - RouterState.of(RouteState.empty(), 1) - ); - - // When - service.loadJobExecutions(model); - - // Then - expect(componentServiceStub.load).toHaveBeenCalledWith(model.getComponentState()); - expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith(FETCH_DATA_JOB_EXECUTIONS, model.getComponentState()); - }); - }); - - describe('|updateJob|', () => { - it('should verify will invoke expected method', () => { - // Given - const model = ComponentModel.of( - ComponentStateImpl.of({ - id: 'test-component' - }), - RouterState.of(RouteState.empty(), 1) - ); - - // When - service.updateJob(model, TASK_UPDATE_JOB_DESCRIPTION); - - // Then - expect(componentServiceStub.load).toHaveBeenCalledWith(model.getComponentState()); - expect(componentServiceStub.dispatchAction).toHaveBeenCalledWith( - UPDATE_DATA_JOB, - model.getComponentState(), - TASK_UPDATE_JOB_DESCRIPTION - ); - }); - }); - - describe('|getNotifiedForRunningJobExecutionId|', () => { - it('should verify will return Observable', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const asObservableSpy = spyOn((service as DataJobsServiceImpl)['_runningJobExecutionId'], 'asObservable').and.callThrough(); - - // When - const observable = service.getNotifiedForRunningJobExecutionId(); - - // Then - expect(observable).toBeInstanceOf(Observable); - expect(asObservableSpy).toHaveBeenCalled(); - }); - }); - - describe('|notifyForRunningJobExecutionId|', () => { - it('should verify will notify correct Subject', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const nextSpy = spyOn((service as DataJobsServiceImpl)['_runningJobExecutionId'], 'next').and.callThrough(); - - // When - service.notifyForRunningJobExecutionId('xy'); - - // Then - expect(nextSpy).toHaveBeenCalledWith('xy'); - }); - }); - - describe('|getNotifiedForJobExecutions|', () => { - it('should verify will return Observable', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const asObservableSpy = spyOn((service as DataJobsServiceImpl)['_jobExecutions'], 'asObservable').and.callThrough(); - - // When - const observable = service.getNotifiedForJobExecutions(); - - // Then - expect(observable).toBeInstanceOf(Observable); - expect(asObservableSpy).toHaveBeenCalled(); - }); - }); - - describe('|notifyForJobExecutions|', () => { - it('should verify will notify correct Subject', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const nextSpy = spyOn((service as DataJobsServiceImpl)['_jobExecutions'], 'next').and.callThrough(); - - // When - service.notifyForJobExecutions([null]); - - // Then - expect(nextSpy).toHaveBeenCalledWith([null]); - }); - }); - - describe('|getNotifiedForTeamImplicitly|', () => { - it('should verify will return Observable', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const asObservableSpy = spyOn((service as DataJobsServiceImpl)['_implicitTeam'], 'asObservable').and.callThrough(); - - // When - const observable = service.getNotifiedForTeamImplicitly(); - - // Then - expect(observable).toBeInstanceOf(Observable); - expect(asObservableSpy).toHaveBeenCalled(); - }); - }); - - describe('|notifyForTeamImplicitly|', () => { - it('should verify will notify correct BehaviorSubject', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const nextSpy = spyOn((service as DataJobsServiceImpl)['_implicitTeam'], 'next').and.callThrough(); - - // When - service.notifyForTeamImplicitly('teamZero'); - - // Then - expect(nextSpy).toHaveBeenCalledWith('teamZero'); - }); - }); + describe("|getNotifiedForTeamImplicitly|", () => { + it("should verify will return Observable", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const asObservableSpy = spyOn( + (service as DataJobsServiceImpl)["_implicitTeam"], + "asObservable", + ).and.callThrough(); + + // When + const observable = service.getNotifiedForTeamImplicitly(); + + // Then + expect(observable).toBeInstanceOf(Observable); + expect(asObservableSpy).toHaveBeenCalled(); + }); + }); + + describe("|notifyForTeamImplicitly|", () => { + it("should verify will notify correct BehaviorSubject", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const nextSpy = spyOn( + (service as DataJobsServiceImpl)["_implicitTeam"], + "next", + ).and.callThrough(); + + // When + service.notifyForTeamImplicitly("teamZero"); + + // Then + expect(nextSpy).toHaveBeenCalledWith("teamZero"); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.ts index 3e264a465f..3f0b44587a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/data-jobs.service.ts @@ -5,154 +5,172 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; -import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from "rxjs"; -import { ComponentModel, ComponentService } from '@versatiledatakit/shared'; +import { ComponentModel, ComponentService } from "@versatiledatakit/shared"; -import { FETCH_DATA_JOB, FETCH_DATA_JOB_EXECUTIONS, FETCH_DATA_JOBS, UPDATE_DATA_JOB } from '../state/actions'; -import { DataJobUpdateTasks } from '../state/tasks'; +import { + FETCH_DATA_JOB, + FETCH_DATA_JOB_EXECUTIONS, + FETCH_DATA_JOBS, + UPDATE_DATA_JOB, +} from "../state/actions"; +import { DataJobUpdateTasks } from "../state/tasks"; -import { DataJobExecutions } from '../model'; +import { DataJobExecutions } from "../model"; export abstract class DataJobsService { - /** - * ** Trigger Action for loading DataJobs data. - */ - abstract loadJobs(model: ComponentModel): void; - - /** - * ** Trigger Actions to load all necessary data for Data Job. - */ - abstract loadJob(model: ComponentModel): void; - - /** - * ** Trigger Action for loading Data Job executions data. - */ - abstract loadJobExecutions(model: ComponentModel): void; - - /** - * ** Trigger Action update Job. - */ - abstract updateJob(model: ComponentModel, task: DataJobUpdateTasks): void; - - /** - * ** Returns Observable(Subject) that fires when Running Job Execution ID change. - */ - abstract getNotifiedForRunningJobExecutionId(): Observable; - - /** - * ** Send new event to Observable stream. - */ - abstract notifyForRunningJobExecutionId(id: string): void; - - /** - * ** Returns Observable(Subject) that fires with new Job Executions. - */ - abstract getNotifiedForJobExecutions(): Observable; - - /** - * ** Send new event to Observable stream. - */ - abstract notifyForJobExecutions(executions: DataJobExecutions): void; - - /** - * ** Returns Observable(BehaviorSubject) that fires with team name implicitly. - */ - abstract getNotifiedForTeamImplicitly(): Observable; - - /** - * ** Send new event to Observable stream. - */ - abstract notifyForTeamImplicitly(team: string): void; + /** + * ** Trigger Action for loading DataJobs data. + */ + abstract loadJobs(model: ComponentModel): void; + + /** + * ** Trigger Actions to load all necessary data for Data Job. + */ + abstract loadJob(model: ComponentModel): void; + + /** + * ** Trigger Action for loading Data Job executions data. + */ + abstract loadJobExecutions(model: ComponentModel): void; + + /** + * ** Trigger Action update Job. + */ + abstract updateJob(model: ComponentModel, task: DataJobUpdateTasks): void; + + /** + * ** Returns Observable(Subject) that fires when Running Job Execution ID change. + */ + abstract getNotifiedForRunningJobExecutionId(): Observable; + + /** + * ** Send new event to Observable stream. + */ + abstract notifyForRunningJobExecutionId(id: string): void; + + /** + * ** Returns Observable(Subject) that fires with new Job Executions. + */ + abstract getNotifiedForJobExecutions(): Observable; + + /** + * ** Send new event to Observable stream. + */ + abstract notifyForJobExecutions(executions: DataJobExecutions): void; + + /** + * ** Returns Observable(BehaviorSubject) that fires with team name implicitly. + */ + abstract getNotifiedForTeamImplicitly(): Observable; + + /** + * ** Send new event to Observable stream. + */ + abstract notifyForTeamImplicitly(team: string): void; } @Injectable() export class DataJobsServiceImpl extends DataJobsService { - private readonly _runningJobExecutionId: Subject; - private readonly _jobExecutions: Subject; - private readonly _implicitTeam: BehaviorSubject; - - /** - * ** Constructor. - */ - constructor(private readonly componentService: ComponentService) { - super(); - - this._runningJobExecutionId = new Subject(); - this._jobExecutions = new Subject(); - this._implicitTeam = new BehaviorSubject(undefined); - } - - /** - * @inheritDoc - */ - loadJobs(model: ComponentModel): void { - this.componentService.load(model.getComponentState()); - this.componentService.dispatchAction(FETCH_DATA_JOBS, model.getComponentState()); - } - - loadJob(model: ComponentModel): void { - this.componentService.load(model.getComponentState()); - this.componentService.dispatchAction(FETCH_DATA_JOB, model.getComponentState()); - } - - /** - * @inheritDoc - */ - loadJobExecutions(model: ComponentModel): void { - this.componentService.load(model.getComponentState()); - this.componentService.dispatchAction(FETCH_DATA_JOB_EXECUTIONS, model.getComponentState()); - } - - /** - * @inheritDoc - */ - updateJob(model: ComponentModel, task: DataJobUpdateTasks): void { - this.componentService.load(model.getComponentState()); - this.componentService.dispatchAction(UPDATE_DATA_JOB, model.getComponentState(), task); - } - - /** - * @inheritDoc - */ - getNotifiedForJobExecutions(): Observable { - return this._jobExecutions.asObservable(); - } - - /** - * @inheritDoc - */ - notifyForJobExecutions(executions: DataJobExecutions): void { - this._jobExecutions.next(executions); - } - - /** - * @inheritDoc - */ - getNotifiedForRunningJobExecutionId(): Observable { - return this._runningJobExecutionId.asObservable(); - } - - /** - * @inheritDoc - */ - notifyForRunningJobExecutionId(id: string): void { - this._runningJobExecutionId.next(id); - } - - /** - * @inheritDoc - */ - getNotifiedForTeamImplicitly(): Observable { - return this._implicitTeam.asObservable(); - } - - /** - * @inheritDoc - */ - notifyForTeamImplicitly(team: string): void { - this._implicitTeam.next(team); - } + private readonly _runningJobExecutionId: Subject; + private readonly _jobExecutions: Subject; + private readonly _implicitTeam: BehaviorSubject; + + /** + * ** Constructor. + */ + constructor(private readonly componentService: ComponentService) { + super(); + + this._runningJobExecutionId = new Subject(); + this._jobExecutions = new Subject(); + this._implicitTeam = new BehaviorSubject(undefined); + } + + /** + * @inheritDoc + */ + loadJobs(model: ComponentModel): void { + this.componentService.load(model.getComponentState()); + this.componentService.dispatchAction( + FETCH_DATA_JOBS, + model.getComponentState(), + ); + } + + loadJob(model: ComponentModel): void { + this.componentService.load(model.getComponentState()); + this.componentService.dispatchAction( + FETCH_DATA_JOB, + model.getComponentState(), + ); + } + + /** + * @inheritDoc + */ + loadJobExecutions(model: ComponentModel): void { + this.componentService.load(model.getComponentState()); + this.componentService.dispatchAction( + FETCH_DATA_JOB_EXECUTIONS, + model.getComponentState(), + ); + } + + /** + * @inheritDoc + */ + updateJob(model: ComponentModel, task: DataJobUpdateTasks): void { + this.componentService.load(model.getComponentState()); + this.componentService.dispatchAction( + UPDATE_DATA_JOB, + model.getComponentState(), + task, + ); + } + + /** + * @inheritDoc + */ + getNotifiedForJobExecutions(): Observable { + return this._jobExecutions.asObservable(); + } + + /** + * @inheritDoc + */ + notifyForJobExecutions(executions: DataJobExecutions): void { + this._jobExecutions.next(executions); + } + + /** + * @inheritDoc + */ + getNotifiedForRunningJobExecutionId(): Observable { + return this._runningJobExecutionId.asObservable(); + } + + /** + * @inheritDoc + */ + notifyForRunningJobExecutionId(id: string): void { + this._runningJobExecutionId.next(id); + } + + /** + * @inheritDoc + */ + getNotifiedForTeamImplicitly(): Observable { + return this._implicitTeam.asObservable(); + } + + /** + * @inheritDoc + */ + notifyForTeamImplicitly(team: string): void { + this._implicitTeam.next(team); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/index.ts index cf83d83e97..477781d0dc 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-base.api.service'; -export * from './data-jobs-public.api.service'; -export * from './data-jobs.api.service'; -export * from './data-jobs.service'; +export * from "./data-jobs-base.api.service"; +export * from "./data-jobs-public.api.service"; +export * from "./data-jobs.api.service"; +export * from "./data-jobs.service"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/public-api.ts index f08a9082f5..8cde2b5ce4 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/services/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs-public.api.service'; +export * from "./data-jobs-public.api.service"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.html index 4bddca7db5..1ab7ee46a6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.html @@ -4,43 +4,43 @@ --> - - - + + + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.scss index 82f6f3a775..c48a58df00 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.scss @@ -4,17 +4,17 @@ */ .schema-confirm-to-prod { - .modal-title { - margin-bottom: 0.5rem; - } + .modal-title { + margin-bottom: 0.5rem; + } - .signpostBtn { - height: 0rem; + .signpostBtn { + height: 0rem; - .btn-info { - margin-top: -0.65rem; - border: 0; - height: 1.2rem; - } + .btn-info { + margin-top: -0.65rem; + border: 0; + height: 1.2rem; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.spec.ts index f26bbfcea3..ff11b8a869 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.spec.ts @@ -3,34 +3,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ConfirmationDialogModalComponent } from './confirmation-dialog-modal.component'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ConfirmationDialogModalComponent } from "./confirmation-dialog-modal.component"; -describe('ConfirmationDialogModalComponent', () => { - let component: ConfirmationDialogModalComponent; - let fixture: ComponentFixture; +describe("ConfirmationDialogModalComponent", () => { + let component: ConfirmationDialogModalComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ConfirmationDialogModalComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ConfirmationDialogModalComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(ConfirmationDialogModalComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmationDialogModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); - describe('confirm', () => { - it('makes expected calls', () => { - spyOn(component.changeStatus, 'emit').and.callThrough(); - component.confirm(); - expect(component.changeStatus.emit).toHaveBeenCalled(); - }); + describe("confirm", () => { + it("makes expected calls", () => { + spyOn(component.changeStatus, "emit").and.callThrough(); + component.confirm(); + expect(component.changeStatus.emit).toHaveBeenCalled(); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.ts index c1eafdb958..c7f3ac4695 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/confirmation-dialog-modal.component.ts @@ -3,27 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { ConfirmationModalOptions } from '../../model/modal-options'; -import { ModalComponentDirective } from '../modal/modal.component'; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { ConfirmationModalOptions } from "../../model/modal-options"; +import { ModalComponentDirective } from "../modal/modal.component"; @Component({ - selector: 'lib-confirmation-dialog-modal', - templateUrl: './confirmation-dialog-modal.component.html', - styleUrls: ['./confirmation-dialog-modal.component.scss'] + selector: "lib-confirmation-dialog-modal", + templateUrl: "./confirmation-dialog-modal.component.html", + styleUrls: ["./confirmation-dialog-modal.component.scss"], }) export class ConfirmationDialogModalComponent extends ModalComponentDirective { - @Input() confirmationInput: string; - @Output() changeStatus: EventEmitter = new EventEmitter(); + @Input() confirmationInput: string; + @Output() changeStatus: EventEmitter = new EventEmitter(); - constructor() { - super(); - this.options = new ConfirmationModalOptions(); - } + constructor() { + super(); + this.options = new ConfirmationModalOptions(); + } - override confirm(): void { - super.confirm(); + override confirm(): void { + super.confirm(); - this.changeStatus.emit(); - } + this.changeStatus.emit(); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/index.ts index c28b78d5f6..bf407c409d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/confirmation-dialog-modal/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './confirmation-dialog-modal.component'; +export * from "./confirmation-dialog-modal.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.html index 010d3811b0..2bd6893e8e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.html @@ -6,33 +6,30 @@ - + - + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.scss index 321379656b..768e3cf3e1 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.scss @@ -4,10 +4,10 @@ */ .datagrid-filter { - outline: none; + outline: none; - .close { - outline: none; - box-shadow: none; - } + .close { + outline: none; + box-shadow: none; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.spec.ts index a52f8af566..e3ef8f4e17 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.spec.ts @@ -3,54 +3,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ClrDatagridFilter } from '@clr/angular'; - -import { ColumnFilterComponent } from './column-filter.component'; - -describe('ColumnFilterComponent', () => { - let component: ColumnFilterComponent; - let fixture: ComponentFixture; - - const TEST_VALUE = 'test_value'; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ColumnFilterComponent], - providers: [ - { - provide: ClrDatagridFilter, - useFactory: () => ({ - setFilter: () => ({}) - }) - } - ] - }).compileComponents(); +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ClrDatagridFilter } from "@clr/angular"; + +import { ColumnFilterComponent } from "./column-filter.component"; + +describe("ColumnFilterComponent", () => { + let component: ColumnFilterComponent; + let fixture: ComponentFixture; + + const TEST_VALUE = "test_value"; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ColumnFilterComponent], + providers: [ + { + provide: ClrDatagridFilter, + useFactory: () => ({ + setFilter: () => ({}), + }), + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ColumnFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("toggle selection", () => { + it("change value", () => { + component.toggleSelection({ + target: { value: TEST_VALUE } as unknown as EventTarget, + } as Event); + expect(component.value).toBe(TEST_VALUE); }); + }); - beforeEach(() => { - fixture = TestBed.createComponent(ColumnFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('toggle selection', () => { - it('change value', () => { - component.toggleSelection({ - target: { value: TEST_VALUE } as unknown as EventTarget - } as Event); - expect(component.value).toBe(TEST_VALUE); - }); - }); - - describe('clean filter', () => { - it('remove value', () => { - component.cleanFilter(); - expect(component.value).toBe(null); - }); + describe("clean filter", () => { + it("remove value", () => { + component.cleanFilter(); + expect(component.value).toBe(null); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.ts index 7578bb7be9..2a811d65eb 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/column-filter.component.ts @@ -3,71 +3,82 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef, ViewEncapsulation } from '@angular/core'; +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + TemplateRef, + ViewEncapsulation, +} from "@angular/core"; -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject } from "rxjs"; -import { ClrDatagridFilter, ClrDatagridFilterInterface } from '@clr/angular'; +import { ClrDatagridFilter, ClrDatagridFilterInterface } from "@clr/angular"; -import { DataJob } from '../../../../model'; +import { DataJob } from "../../../../model"; @Component({ - selector: 'lib-column-filter', - templateUrl: './column-filter.component.html', - styleUrls: ['./column-filter.component.scss'], - encapsulation: ViewEncapsulation.None + selector: "lib-column-filter", + templateUrl: "./column-filter.component.html", + styleUrls: ["./column-filter.component.scss"], + encapsulation: ViewEncapsulation.None, }) -export class ColumnFilterComponent implements ClrDatagridFilterInterface, OnChanges { - @Input() property: string; - @Input() listOfOptions: string[]; - @Input() isExecutionStatus = false; - - @Input() optionRenderer: TemplateRef = null; - - private _changesSubject = new Subject(); - - @Input() value: string; - @Output() valueChange = new EventEmitter(); - - // We do not want to expose the Subject itself, but the Observable which is read-only - get changes(): Observable { - return this._changesSubject.asObservable(); - } - - constructor(private filterContainer: ClrDatagridFilter) { - filterContainer.setFilter(this); - } - - isActive(): boolean { - return !!this.value; - } - - accepts(_item: DataJob): boolean { - return true; - } - - toggleSelection($event: Event) { - this._setValue(($event.target as HTMLInputElement).value); - } - - cleanFilter() { - this._setValue(null); - } - - isValueSelected(value: string) { - return this.value === value; - } - - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges): void { - this._changesSubject.next(changes['value'].currentValue as string); - } - - private _setValue(value: string): void { - this.value = value; - this.valueChange.emit(this.value); - this._changesSubject.next(this.value); - } +export class ColumnFilterComponent + implements ClrDatagridFilterInterface, OnChanges +{ + @Input() property: string; + @Input() listOfOptions: string[]; + @Input() isExecutionStatus = false; + + @Input() optionRenderer: TemplateRef = null; + + private _changesSubject = new Subject(); + + @Input() value: string; + @Output() valueChange = new EventEmitter(); + + // We do not want to expose the Subject itself, but the Observable which is read-only + get changes(): Observable { + return this._changesSubject.asObservable(); + } + + constructor(private filterContainer: ClrDatagridFilter) { + filterContainer.setFilter(this); + } + + isActive(): boolean { + return !!this.value; + } + + accepts(_item: DataJob): boolean { + return true; + } + + toggleSelection($event: Event) { + this._setValue(($event.target as HTMLInputElement).value); + } + + cleanFilter() { + this._setValue(null); + } + + isValueSelected(value: string) { + return this.value === value; + } + + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + this._changesSubject.next(changes["value"].currentValue as string); + } + + private _setValue(value: string): void { + this.value = value; + this.valueChange.emit(this.value); + this._changesSubject.next(this.value); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/index.ts index aee0c8c5dd..32562c295a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/column-filter/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './column-filter.component'; +export * from "./column-filter.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.html index a1bab5c6be..d7fefa7c76 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.html @@ -4,97 +4,93 @@ -->
-
- +
+ - + + -
- + +
- -
+
+ -
- + +
- -
+
+ + + +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.scss index 76a4a39ca6..1292a498a9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.scss @@ -4,85 +4,85 @@ */ .grid-actions-container { - display: flex; - justify-content: space-between; - align-content: center; - flex-direction: row; - margin-top: 20px; - min-height: 1.5rem; + display: flex; + justify-content: space-between; + align-content: center; + flex-direction: row; + margin-top: 20px; + min-height: 1.5rem; - .btn-actions-container { - .btn { - margin: 0.25rem 0; - padding: 0 0.5rem; - } + .btn-actions-container { + .btn { + margin: 0.25rem 0; + padding: 0 0.5rem; } + } - > .btn-actions-container { - button { - &:first-child { - padding-left: 0; - } - } + > .btn-actions-container { + button { + &:first-child { + padding-left: 0; + } } + } - .btn-filters-container { - display: flex; - flex-grow: 1; - justify-content: flex-end; - } + .btn-filters-container { + display: flex; + flex-grow: 1; + justify-content: flex-end; + } - .btn-search-container { - display: inline-flex; + .btn-search-container { + display: inline-flex; - .search-container { - display: inline-flex; - margin: 0 !important; - transform: translateY(0.15rem); + .search-container { + display: inline-flex; + margin: 0 !important; + transform: translateY(0.15rem); - input { - &.clr-input { - padding-top: 2px; - } - } + input { + &.clr-input { + padding-top: 2px; } + } } + } - .tooltip-content { - text-transform: initial; - font-size: 11px !important; - } + .tooltip-content { + text-transform: initial; + font-size: 11px !important; + } - > * { - margin-right: 1rem; + > * { + margin-right: 1rem; - &:last-child { - margin-right: 0; - } + &:last-child { + margin-right: 0; } + } } .custom-buttons { - .btn { - &:not(.custom-btn) { - margin: initial !important; - padding: initial !important; - } + .btn { + &:not(.custom-btn) { + margin: initial !important; + padding: initial !important; } + } } .btn { - &.btn-link { - &.btn-link-red { - color: #e62700; + &.btn-link { + &.btn-link-red { + color: #e62700; - &:hover { - color: #a32100; - } + &:hover { + color: #a32100; + } - &:disabled { - color: #565656; - } - } + &:disabled { + color: #565656; + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.spec.ts index abeaee5c82..262e928862 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.spec.ts @@ -3,40 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GridActionComponent } from './grid-action.component'; - -describe('GridActionComponent', () => { - let component: GridActionComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [GridActionComponent] - }); - fixture = TestBed.createComponent(GridActionComponent); - component = fixture.componentInstance; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { GridActionComponent } from "./grid-action.component"; + +describe("GridActionComponent", () => { + let component: GridActionComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [GridActionComponent], }); + fixture = TestBed.createComponent(GridActionComponent); + component = fixture.componentInstance; + }); - it('can load instance', () => { - expect(component).toBeTruthy(); - }); + it("can load instance", () => { + expect(component).toBeTruthy(); + }); - it(`id has default value`, () => { - expect(component.id).toBeDefined(); - }); + it(`id has default value`, () => { + expect(component.id).toBeDefined(); + }); - it(`addId has default value`, () => { - expect(component.addId).toBeDefined(); - }); + it(`addId has default value`, () => { + expect(component.addId).toBeDefined(); + }); - it(`editId has default value`, () => { - expect(component.editId).toBeDefined(); - }); + it(`editId has default value`, () => { + expect(component.editId).toBeDefined(); + }); - it(`removeId has default value`, () => { - expect(component.removeId).toBeDefined(); - }); + it(`removeId has default value`, () => { + expect(component.removeId).toBeDefined(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.ts index 1cf51ed933..fef2b8c26d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/grid-action.component.ts @@ -3,103 +3,114 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AfterViewInit, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { + AfterViewInit, + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewEncapsulation, +} from "@angular/core"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { QuickFilterChangeEvent, QuickFilters } from '../../quick-filters'; +import { QuickFilterChangeEvent, QuickFilters } from "../../quick-filters"; @Component({ - selector: 'lib-grid-action', - templateUrl: './grid-action.component.html', - styleUrls: ['./grid-action.component.scss'], - encapsulation: ViewEncapsulation.None + selector: "lib-grid-action", + templateUrl: "./grid-action.component.html", + styleUrls: ["./grid-action.component.scss"], + encapsulation: ViewEncapsulation.None, }) export class GridActionComponent implements AfterViewInit, OnChanges { - @Input() id = 'lib-ga-search-id'; - @Input() addId = 'lib-ga-add-id'; - @Input() editId = 'lib-ga-edit-id'; - @Input() removeId = 'lib-ga-remove-id'; - - @Input() addLabel: string; - @Input() editLabel: string; - @Input() removeLabel: string; - - @Input() addTooltip: string; - @Input() editTooltip: string; - @Input() removeTooltip: string; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - @Input() selectedValue: any | any[]; - @Input() searchQueryValue = ''; - - @Input() disableAdd: boolean; - @Input() disableEdit: boolean; - @Input() disableRemove: boolean; - - /** - * ** Proxy config for QuickFilters component. - */ - @Input() quickFilters: QuickFilters; - @Input() suppressQuickFilterChangeEvent: boolean; - - /** - * ** Flag that indicates actionable elements should be disabled. - */ - @Input() disableActionableElements = false; - - /** - * ** Proxy emitter from QuickFilters component. - */ - @Output() quickFilterChange = new EventEmitter(); - - @Output() search: EventEmitter = new EventEmitter(); - @Output() add: EventEmitter = new EventEmitter(); - /* eslint-disable @typescript-eslint/no-explicit-any */ - @Output() edit: EventEmitter = new EventEmitter(); - @Output() remove: EventEmitter = new EventEmitter(); - /* eslint-enable @typescript-eslint/no-explicit-any */ - - queryValue: string; - - ngAfterViewInit(): void { - this.setQueryValue(); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes['searchQueryValue']) { - this.setQueryValue(); - } - } - - get editDisabled(): boolean { - return ( - CollectionsUtil.isNil(this.selectedValue) || - (CollectionsUtil.isString(this.selectedValue) && this.selectedValue.length === 0) || - this.disableEdit - ); - } - - get addDisabled(): boolean { - return this.disableAdd; - } - - get removeDisabled(): boolean { - return ( - CollectionsUtil.isNil(this.selectedValue) || - (CollectionsUtil.isString(this.selectedValue) && this.selectedValue.length === 0) || - this.disableRemove - ); - } - - /** - * vdk-search is being broken for one-way binding related to an input [searchQueryValue] - * this fix is a workaround (adding a delay of 1 millisecond to set queryValue, looks like - * needs to run in a separate thread) - */ - private setQueryValue() { - setTimeout(() => { - this.queryValue = this.searchQueryValue; - }, 1); + @Input() id = "lib-ga-search-id"; + @Input() addId = "lib-ga-add-id"; + @Input() editId = "lib-ga-edit-id"; + @Input() removeId = "lib-ga-remove-id"; + + @Input() addLabel: string; + @Input() editLabel: string; + @Input() removeLabel: string; + + @Input() addTooltip: string; + @Input() editTooltip: string; + @Input() removeTooltip: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + @Input() selectedValue: any | any[]; + @Input() searchQueryValue = ""; + + @Input() disableAdd: boolean; + @Input() disableEdit: boolean; + @Input() disableRemove: boolean; + + /** + * ** Proxy config for QuickFilters component. + */ + @Input() quickFilters: QuickFilters; + @Input() suppressQuickFilterChangeEvent: boolean; + + /** + * ** Flag that indicates actionable elements should be disabled. + */ + @Input() disableActionableElements = false; + + /** + * ** Proxy emitter from QuickFilters component. + */ + @Output() quickFilterChange = new EventEmitter(); + + @Output() search: EventEmitter = new EventEmitter(); + @Output() add: EventEmitter = new EventEmitter(); + /* eslint-disable @typescript-eslint/no-explicit-any */ + @Output() edit: EventEmitter = new EventEmitter(); + @Output() remove: EventEmitter = new EventEmitter(); + /* eslint-enable @typescript-eslint/no-explicit-any */ + + queryValue: string; + + ngAfterViewInit(): void { + this.setQueryValue(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes["searchQueryValue"]) { + this.setQueryValue(); } + } + + get editDisabled(): boolean { + return ( + CollectionsUtil.isNil(this.selectedValue) || + (CollectionsUtil.isString(this.selectedValue) && + this.selectedValue.length === 0) || + this.disableEdit + ); + } + + get addDisabled(): boolean { + return this.disableAdd; + } + + get removeDisabled(): boolean { + return ( + CollectionsUtil.isNil(this.selectedValue) || + (CollectionsUtil.isString(this.selectedValue) && + this.selectedValue.length === 0) || + this.disableRemove + ); + } + + /** + * vdk-search is being broken for one-way binding related to an input [searchQueryValue] + * this fix is a workaround (adding a delay of 1 millisecond to set queryValue, looks like + * needs to run in a separate thread) + */ + private setQueryValue() { + setTimeout(() => { + this.queryValue = this.searchQueryValue; + }, 1); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/index.ts index 0b8f0005b5..ce776e0c3a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/grid-action/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './grid-action.component'; +export * from "./grid-action.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/index.ts index d3248b2bd9..9590c8e28e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/data-grid/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './grid-action'; -export * from './column-filter'; +export * from "./grid-action"; +export * from "./column-filter"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.html index db3df86d6e..5c7909a150 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.html @@ -4,45 +4,45 @@ --> - + - - + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.spec.ts index f9fdfbe19d..25246da6bd 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.spec.ts @@ -3,40 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { DeleteModalComponent } from './delete-modal.component'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { DeleteModalComponent } from "./delete-modal.component"; -describe('DeleteModalComponent', () => { - let component: DeleteModalComponent; - let fixture: ComponentFixture; +describe("DeleteModalComponent", () => { + let component: DeleteModalComponent; + let fixture: ComponentFixture; - beforeEach(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DeleteModalComponent] - }); - fixture = TestBed.createComponent(DeleteModalComponent); - component = fixture.componentInstance; + beforeEach(() => { + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [DeleteModalComponent], }); + fixture = TestBed.createComponent(DeleteModalComponent); + component = fixture.componentInstance; + }); - it('can load instance', () => { - expect(component).toBeTruthy(); - }); + it("can load instance", () => { + expect(component).toBeTruthy(); + }); - describe('confirm', () => { - it('makes expected calls', () => { - spyOn(component, 'close').and.callThrough(); - component.confirm(); - expect(component.close).toHaveBeenCalled(); - }); + describe("confirm", () => { + it("makes expected calls", () => { + spyOn(component, "close").and.callThrough(); + component.confirm(); + expect(component.close).toHaveBeenCalled(); }); + }); - describe('cancel', () => { - it('makes expected calls', () => { - spyOn(component, 'close').and.callThrough(); - component.cancel(); - expect(component.close).toHaveBeenCalled(); - }); + describe("cancel", () => { + it("makes expected calls", () => { + spyOn(component, "close").and.callThrough(); + component.cancel(); + expect(component.close).toHaveBeenCalled(); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.ts index 1a82fa8803..8bf6442a97 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/delete-modal.component.ts @@ -3,32 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, Output } from "@angular/core"; -import { DeleteModalOptions } from '../../model'; +import { DeleteModalOptions } from "../../model"; -import { ModalComponentDirective } from '../modal'; +import { ModalComponentDirective } from "../modal"; @Component({ - selector: 'lib-delete-modal', - templateUrl: './delete-modal.component.html', - styleUrls: ['./delete-modal.component.css'] + selector: "lib-delete-modal", + templateUrl: "./delete-modal.component.html", + styleUrls: ["./delete-modal.component.css"], }) export class DeleteModalComponent extends ModalComponentDirective { - @Output() delete: EventEmitter = new EventEmitter(); + @Output() delete: EventEmitter = new EventEmitter(); - constructor() { - super(); - this.options = new DeleteModalOptions(); - } + constructor() { + super(); + this.options = new DeleteModalOptions(); + } - /** - * emit that the user confirmed that it want to delete the item - * and close the modal - */ - override confirm(): void { - super.confirm(); + /** + * emit that the user confirmed that it want to delete the item + * and close the modal + */ + override confirm(): void { + super.confirm(); - this.delete.emit(); - } + this.delete.emit(); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/index.ts index 9fe8cb12c0..75f61930e7 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/delete-modal/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './delete-modal.component'; +export * from "./delete-modal.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.html index 2e8ae41017..b9ae5ce927 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.html @@ -6,26 +6,26 @@
- - + + - - - - - + + + + +
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.scss index 6b38c17232..e603b1714f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.scss @@ -4,19 +4,19 @@ */ .empty-state-container { - vdk-empty-state-placeholder { - text-align: center; - ::ng-deep h2 { - font-size: 18px !important; - font-weight: 500; - margin-top: 0.1rem !important; - line-height: 1rem; - } + vdk-empty-state-placeholder { + text-align: center; + ::ng-deep h2 { + font-size: 18px !important; + font-weight: 500; + margin-top: 0.1rem !important; + line-height: 1rem; + } - ::ng-deep h3 { - font-size: 13px !important; - line-height: 1rem; - margin-top: 0.3rem; - } + ::ng-deep h3 { + font-size: 13px !important; + line-height: 1rem; + margin-top: 0.3rem; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.spec.ts index 850cfc3b1d..3f94a24ac5 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.spec.ts @@ -9,27 +9,27 @@ * @format */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { EmptyStateComponent } from './empty-state.component'; +import { EmptyStateComponent } from "./empty-state.component"; -describe('EmptyStateComponent', () => { - let component: EmptyStateComponent; - let fixture: ComponentFixture; +describe("EmptyStateComponent", () => { + let component: EmptyStateComponent; + let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [EmptyStateComponent] - }).compileComponents(); - })); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [EmptyStateComponent], + }).compileComponents(); + })); - beforeEach(() => { - fixture = TestBed.createComponent(EmptyStateComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.ts index 45ade7179a..10da692ab9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/empty-state.component.ts @@ -5,21 +5,21 @@ /** @format */ -import { Component, Input } from '@angular/core'; +import { Component, Input } from "@angular/core"; @Component({ - selector: 'lib-empty-state', - templateUrl: './empty-state.component.html', - styleUrls: ['./empty-state.component.scss'] + selector: "lib-empty-state", + templateUrl: "./empty-state.component.html", + styleUrls: ["./empty-state.component.scss"], }) export class EmptyStateComponent { - @Input() title = 'Empty State'; - @Input() description = 'Description'; - @Input() width = 256; - @Input() imgSrc: string; - @Input() hideImage = false; - @Input() opacity = 1; - @Input() animSrc = 'assets/animations/no-events-in-timeframe-animation.json'; - @Input() marginTop = '3rem'; - @Input() marginBottom = '20px'; + @Input() title = "Empty State"; + @Input() description = "Description"; + @Input() width = 256; + @Input() imgSrc: string; + @Input() hideImage = false; + @Input() opacity = 1; + @Input() animSrc = "assets/animations/no-events-in-timeframe-animation.json"; + @Input() marginTop = "3rem"; + @Input() marginBottom = "20px"; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/index.ts index f914b7eb01..9d85b2ca72 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/empty-state/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './empty-state.component'; +export * from "./empty-state.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.html index cc77eeeedc..3de1100f0b 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.html @@ -6,183 +6,186 @@
+
+ - + +
  • +
    -
    - Scheduled - {{ next | date : "MMM d, y, hh:mm a" : "UTC" }} UTC -
    -
  • + + +
    + Scheduled + {{ next | date: "MMM d, y, hh:mm a" : "UTC" }} UTC +
    + diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.scss index f7223eab76..3ea553a061 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.scss @@ -3,56 +3,56 @@ * SPDX-License-Identifier: Apache-2.0 */ -.clr-timeline-step clr-icon[shape='success-standard'] { - color: var(--clr-timeline-success-step-color, #5eb715); +.clr-timeline-step clr-icon[shape="success-standard"] { + color: var(--clr-timeline-success-step-color, #5eb715); } -.clr-timeline-step clr-icon[shape='error-standard'] { - color: var(--clr-timeline-error-step-color, #c21d00); +.clr-timeline-step clr-icon[shape="error-standard"] { + color: var(--clr-timeline-error-step-color, #c21d00); } .clr-timeline-step clr-icon { - height: 1.8rem; - width: 1.8rem; - min-height: 1.8rem; - min-width: 1.8rem; + height: 1.8rem; + width: 1.8rem; + min-height: 1.8rem; + min-width: 1.8rem; } .clr-timeline-horizontal { - padding-top: 35px; + padding-top: 35px; } .manual-execution-label { - margin-top: 5px; + margin-top: 5px; } .clr-timeline { - .clr-timeline__step-header--underline-dotted { - text-decoration: underline; - text-decoration-style: dotted; + .clr-timeline__step-header--underline-dotted { + text-decoration: underline; + text-decoration-style: dotted; + } + + .clr-timeline__log-link { + min-height: 0 !important; + min-width: 0 !important; + } + + .clr-timeline__element--display-block { + display: block; + width: 200px; + + span { + word-break: break-word; + + .btn-show-more { + padding: 0; + margin: 0; + } } + } - .clr-timeline__log-link { - min-height: 0 !important; - min-width: 0 !important; - } - - .clr-timeline__element--display-block { - display: block; - width: 200px; - - span { - word-break: break-word; - - .btn-show-more { - padding: 0; - margin: 0; - } - } - } - - .clr-timeline__duration-tag { - font-weight: bold; - text-decoration: underline; - } + .clr-timeline__duration-tag { + font-weight: bold; + text-decoration: underline; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.spec.ts index dacb9f7cf3..7c4cf428ed 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.spec.ts @@ -3,70 +3,82 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ExecutionsTimelineComponent } from './executions-timeline.component'; +import { ExecutionsTimelineComponent } from "./executions-timeline.component"; -import { DATA_PIPELINES_CONFIGS, DataJobExecution, DataJobExecutionType } from '../../../model'; +import { + DATA_PIPELINES_CONFIGS, + DataJobExecution, + DataJobExecutionType, +} from "../../../model"; -describe('ExecutionsTimelineComponent', () => { - let component: ExecutionsTimelineComponent; - let fixture: ComponentFixture; +describe("ExecutionsTimelineComponent", () => { + let component: ExecutionsTimelineComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ExecutionsTimelineComponent], - providers: [ - { - provide: DATA_PIPELINES_CONFIGS, - useFactory: () => ({ - defaultOwnerTeamName: 'all', - manageConfig: { - allowKeyTabDownloads: true, - allowExecuteNow: true - }, - healthStatusUrl: 'baseUrl' - }) - } - ] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ExecutionsTimelineComponent], + providers: [ + { + provide: DATA_PIPELINES_CONFIGS, + useFactory: () => ({ + defaultOwnerTeamName: "all", + manageConfig: { + allowKeyTabDownloads: true, + allowExecuteNow: true, + }, + healthStatusUrl: "baseUrl", + }), + }, + ], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(ExecutionsTimelineComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(ExecutionsTimelineComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); - it('should create', () => { - const mockExec = { - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - startedBy: 'manual/auser' - } as DataJobExecution; - expect(component.getManualExecutedByTitle(mockExec)).toBe(ExecutionsTimelineComponent.manualRunKnownUser + ' auser'); + it("should create", () => { + const mockExec = { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + startedBy: "manual/auser", + } as DataJobExecution; + expect(component.getManualExecutedByTitle(mockExec)).toBe( + ExecutionsTimelineComponent.manualRunKnownUser + " auser", + ); - mockExec.startedBy = 'manual/manual'; - mockExec.type = DataJobExecutionType.MANUAL; - expect(component.getManualExecutedByTitle(mockExec)).toBe(ExecutionsTimelineComponent.manualRunKnownUser + ' manual'); + mockExec.startedBy = "manual/manual"; + mockExec.type = DataJobExecutionType.MANUAL; + expect(component.getManualExecutedByTitle(mockExec)).toBe( + ExecutionsTimelineComponent.manualRunKnownUser + " manual", + ); - mockExec.startedBy = 'scheduled/runtime'; - mockExec.type = DataJobExecutionType.SCHEDULED; - expect(component.getManualExecutedByTitle(mockExec)).toContain(ExecutionsTimelineComponent.manualRunNoUser); + mockExec.startedBy = "scheduled/runtime"; + mockExec.type = DataJobExecutionType.SCHEDULED; + expect(component.getManualExecutedByTitle(mockExec)).toContain( + ExecutionsTimelineComponent.manualRunNoUser, + ); - mockExec.startedBy = 'scheduled/runtime'; - mockExec.type = null; - expect(component.getManualExecutedByTitle(mockExec)).toContain(ExecutionsTimelineComponent.manualRunNoUser); - }); + mockExec.startedBy = "scheduled/runtime"; + mockExec.type = null; + expect(component.getManualExecutedByTitle(mockExec)).toContain( + ExecutionsTimelineComponent.manualRunNoUser, + ); + }); - describe('Methods::', () => { - // TODO write some unit tests - it('no-op', () => { - // No-op. - expect(true).toBeTrue(); - }); + describe("Methods::", () => { + // TODO write some unit tests + it("no-op", () => { + // No-op. + expect(true).toBeTrue(); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.ts index b99f82e710..67915c36b6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/executions-timeline.component.ts @@ -3,76 +3,94 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Inject, + Input, + OnInit, +} from "@angular/core"; import { - DATA_PIPELINES_CONFIGS, - DataJobExecution, - DataJobExecutions, - DataJobExecutionStatus, - DataJobExecutionType, - DataPipelinesConfig -} from '../../../model'; + DATA_PIPELINES_CONFIGS, + DataJobExecution, + DataJobExecutions, + DataJobExecutionStatus, + DataJobExecutionType, + DataPipelinesConfig, +} from "../../../model"; @Component({ - selector: 'lib-executions-timeline', - templateUrl: './executions-timeline.component.html', - styleUrls: ['./executions-timeline.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: "lib-executions-timeline", + templateUrl: "./executions-timeline.component.html", + styleUrls: ["./executions-timeline.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ExecutionsTimelineComponent implements OnInit { - static manualRunKnownUser = 'This job is triggered manually by user'; - static manualRunNoUser = 'This job is triggered manually, but there is no info about the user'; + static manualRunKnownUser = "This job is triggered manually by user"; + static manualRunNoUser = + "This job is triggered manually, but there is no info about the user"; - @Input() jobExecutions: DataJobExecutions = []; - @Input() next: Date = null; - @Input() showErrorMessage = false; - showExecutionFullMessage: boolean[]; + @Input() jobExecutions: DataJobExecutions = []; + @Input() next: Date = null; + @Input() showErrorMessage = false; + showExecutionFullMessage: boolean[]; - messageWordsBeforeTruncate = 50; - dataJobExecutionStatus = DataJobExecutionStatus; + messageWordsBeforeTruncate = 50; + dataJobExecutionStatus = DataJobExecutionStatus; - constructor( - @Inject(DATA_PIPELINES_CONFIGS) - public dataPipelinesModuleConfig: DataPipelinesConfig - ) {} + constructor( + @Inject(DATA_PIPELINES_CONFIGS) + public dataPipelinesModuleConfig: DataPipelinesConfig, + ) {} - ngOnInit(): void { - this.showExecutionFullMessage = new Array(this.jobExecutions.length).fill(false); - } + ngOnInit(): void { + this.showExecutionFullMessage = new Array(this.jobExecutions.length).fill( + false, + ); + } - /** - * ** NgFor elements tracking function. - */ - trackByFn(index: number, execution: DataJobExecution): string { - return `${index}|${execution.id}`; - } + /** + * ** NgFor elements tracking function. + */ + trackByFn(index: number, execution: DataJobExecution): string { + return `${index}|${execution.id}`; + } - isExecutionManual(execution: DataJobExecution): boolean { - return execution?.type === DataJobExecutionType.MANUAL; - } + isExecutionManual(execution: DataJobExecution): boolean { + return execution?.type === DataJobExecutionType.MANUAL; + } - getManualExecutedByTitle(execution: DataJobExecution): string { - if (!execution || !execution.startedBy || !execution.startedBy.startsWith('manual/')) { - // execution has no info abot user provided - return ExecutionsTimelineComponent.manualRunNoUser; - } + getManualExecutedByTitle(execution: DataJobExecution): string { + if ( + !execution || + !execution.startedBy || + !execution.startedBy.startsWith("manual/") + ) { + // execution has no info abot user provided + return ExecutionsTimelineComponent.manualRunNoUser; + } - const user = execution.startedBy.replace('manual/', ''); + const user = execution.startedBy.replace("manual/", ""); - return `${ExecutionsTimelineComponent.manualRunKnownUser} ${user}`; - } + return `${ExecutionsTimelineComponent.manualRunKnownUser} ${user}`; + } - isJobStatusSuitableForMessageTooltip(execution: DataJobExecution): boolean { - return ( - execution.status === DataJobExecutionStatus.PLATFORM_ERROR || - execution.status === DataJobExecutionStatus.USER_ERROR || - execution.status === DataJobExecutionStatus.SKIPPED - ); - } + isJobStatusSuitableForMessageTooltip(execution: DataJobExecution): boolean { + return ( + execution.status === DataJobExecutionStatus.PLATFORM_ERROR || + execution.status === DataJobExecutionStatus.USER_ERROR || + execution.status === DataJobExecutionStatus.SKIPPED + ); + } - isJobMessageDifferentFromStatus(execution: DataJobExecution): boolean { - const message = execution.message?.toLowerCase(); - return message !== 'user error' && message !== 'platform error' && message !== 'skipped' && message !== ''; - } + isJobMessageDifferentFromStatus(execution: DataJobExecution): boolean { + const message = execution.message?.toLowerCase(); + return ( + message !== "user error" && + message !== "platform error" && + message !== "skipped" && + message !== "" + ); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/index.ts index 3efa58f2da..4cfd22d42e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/executions-timeline/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './executions-timeline.component'; +export * from "./executions-timeline.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/index.ts index dde361c100..e855bc7c0c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/index.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './confirmation-dialog-modal'; -export * from './delete-modal'; -export * from './executions-timeline'; -export * from './data-grid'; -export * from './modal'; -export * from './quick-filters'; -export * from './status'; -export * from './widget-value'; -export * from './empty-state'; +export * from "./confirmation-dialog-modal"; +export * from "./delete-modal"; +export * from "./executions-timeline"; +export * from "./data-grid"; +export * from "./modal"; +export * from "./quick-filters"; +export * from "./status"; +export * from "./widget-value"; +export * from "./empty-state"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/index.ts index 617ba59125..9e0c479077 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './modal.component'; +export * from "./modal.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/modal.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/modal.component.ts index b697e424fa..fc81385945 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/modal.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/modal/modal.component.ts @@ -3,44 +3,46 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive, EventEmitter, Input, Output } from '@angular/core'; +import { Directive, EventEmitter, Input, Output } from "@angular/core"; -import { TaurusObject } from '@versatiledatakit/shared'; +import { TaurusObject } from "@versatiledatakit/shared"; -import { ModalOptions } from '../../model'; +import { ModalOptions } from "../../model"; @Directive() export abstract class ModalComponentDirective extends TaurusObject { - @Input() options: ModalOptions; + @Input() options: ModalOptions; - @Output() optionsChange: EventEmitter = new EventEmitter(); + @Output() optionsChange: EventEmitter = + new EventEmitter(); - @Output() cancelAction: EventEmitter = new EventEmitter(); + @Output() cancelAction: EventEmitter = + new EventEmitter(); - constructor() { - super(); - } + constructor() { + super(); + } - confirm() { - this.close(); - } + confirm() { + this.close(); + } - /** - * close the modal - */ - close(): void { - if (!this._isNull(this.options)) { - this.options.opened = false; - this.optionsChange.emit(this.options); - } + /** + * close the modal + */ + close(): void { + if (!this._isNull(this.options)) { + this.options.opened = false; + this.optionsChange.emit(this.options); } + } - cancel() { - this.cancelAction.emit(); - this.close(); - } + cancel() { + this.cancelAction.emit(); + this.close(); + } - private _isNull(value: ModalOptions): boolean { - return value === null || value === undefined; - } + private _isNull(value: ModalOptions): boolean { + return value === null || value === undefined; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/index.ts index ec1f690625..f62e64fea6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './quick-filters.component'; -export * from './model'; +export * from "./quick-filters.component"; +export * from "./model"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/index.ts index 6da903fb89..90d10ac6eb 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './quick-filters.model'; +export * from "./quick-filters.model"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/quick-filters.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/quick-filters.model.ts index 54cce248cd..7c1ce1846a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/quick-filters.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/model/quick-filters.model.ts @@ -3,31 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Attributes } from '../../../directives'; +import { Attributes } from "../../../directives"; interface IconAttributes extends Attributes { - style?: string; - title?: string; - class?: string; - shape?: string; - // eslint-disable-next-line @typescript-eslint/naming-convention - 'data-cy'?: string; - size?: number; + style?: string; + title?: string; + class?: string; + shape?: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + "data-cy"?: string; + size?: number; } export interface QuickFilter { - id?: string; - label: string; - icon?: IconAttributes; - active?: boolean; - suppressCancel?: boolean; - onActivate?: () => void; - onDeactivate?: () => void; + id?: string; + label: string; + icon?: IconAttributes; + active?: boolean; + suppressCancel?: boolean; + onActivate?: () => void; + onDeactivate?: () => void; } export type QuickFilters = QuickFilter[]; export interface QuickFilterChangeEvent { - deactivatedFilter: QuickFilter | null; - activatedFilter: QuickFilter | null; + deactivatedFilter: QuickFilter | null; + activatedFilter: QuickFilter | null; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.html index 90de0fc334..5504fc4709 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.html @@ -4,35 +4,34 @@ -->
    -
    - QUICK FILTERS: -
    +
    + QUICK FILTERS: +
    -
    - - +
    + + - {{ filter.label }} - -
    + {{ filter.label }} +
    +
    diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.scss b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.scss index 547b0b7070..8cb6d91104 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.scss @@ -4,41 +4,41 @@ */ :host { - display: flex; - align-items: center; + display: flex; + align-items: center; } .quick-filters { - display: flex; - - .quick-filters__title { - display: inline-flex; - font-size: var(--clr-btn-appearance-standard-font-size, 0.55rem); - text-transform: uppercase; - align-items: center; - margin-right: 0.25rem; - white-space: nowrap; - } + display: flex; + + .quick-filters__title { + display: inline-flex; + font-size: var(--clr-btn-appearance-standard-font-size, 0.55rem); + text-transform: uppercase; + align-items: center; + margin-right: 0.25rem; + white-space: nowrap; + } - .quick-filters__list { - display: inline-flex; - align-items: center; + .quick-filters__list { + display: inline-flex; + align-items: center; - .quick-filters__list-item { - margin: 0 0 0 0.3rem; + .quick-filters__list-item { + margin: 0 0 0 0.3rem; - &.disabled { - cursor: not-allowed; - } + &.disabled { + cursor: not-allowed; + } - &.clickable { - cursor: pointer; - } - } + &.clickable { + cursor: pointer; + } + } - .label-light-blue { - border: 1px solid #4474b9; - background-color: rgb(177, 208, 255); - } + .label-light-blue { + border: 1px solid #4474b9; + background-color: rgb(177, 208, 255); } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.spec.ts index 4d428a6f6e..4996831144 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.spec.ts @@ -3,27 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { QuickFiltersComponent } from './quick-filters.component'; +import { QuickFiltersComponent } from "./quick-filters.component"; -describe('QuickFiltersComponent', () => { - let component: QuickFiltersComponent; - let fixture: ComponentFixture; +describe("QuickFiltersComponent", () => { + let component: QuickFiltersComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [QuickFiltersComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [QuickFiltersComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(QuickFiltersComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(QuickFiltersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.ts index 6180ac0f70..fd0c12cadf 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/quick-filters/quick-filters.component.ts @@ -3,124 +3,136 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, +} from "@angular/core"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { QuickFilter, QuickFilterChangeEvent, QuickFilters } from './model'; +import { QuickFilter, QuickFilterChangeEvent, QuickFilters } from "./model"; @Component({ - selector: 'lib-quick-filters', - templateUrl: './quick-filters.component.html', - styleUrls: ['./quick-filters.component.scss'] + selector: "lib-quick-filters", + templateUrl: "./quick-filters.component.html", + styleUrls: ["./quick-filters.component.scss"], }) export class QuickFiltersComponent implements OnChanges { - /** - * ** Quick Filters array config. - */ - @Input() set quickFilters(filters: QuickFilters) { - this._quickFilters = CollectionsUtil.isArray(filters) ? filters : []; + /** + * ** Quick Filters array config. + */ + @Input() set quickFilters(filters: QuickFilters) { + this._quickFilters = CollectionsUtil.isArray(filters) ? filters : []; + } + + get quickFilters(): QuickFilters { + return this._quickFilters; + } + + /** + * ** Show or hide Label "QUICK FILTERS" before filters list. + * + * - true - Show + * - false - Hide + */ + @Input() showFiltersLabel = false; + + /** + * ** Suppress emitted event when some filter state change. + * + * - true - Event wont be emitted + * - false - Event would be emitted on change + */ + @Input() suppressQuickFilterChangeEvent = false; + + /** + * ** Flag that indicates actionable elements should be disabled. + */ + @Input() disableActionableElements = false; + + /** + * ** Event Emitter for Filter state change. + */ + @Output() quickFilterChange = new EventEmitter(); + + activatedFilter: QuickFilter; + + private _quickFilters: QuickFilters = []; + private _deactivatedFilter: QuickFilter | null = null; + + /** + * ** NgFor elements tracking function. + */ + trackByFn(index: number, filter: QuickFilter): string { + return `${index}|${filter.id}`; + } + + /** + * ** Executed when some filter change it's state. + *

    + * State changes when User click on some Filter or press Enter while it's on focus. + *

    + */ + changeFilter(filter: QuickFilter): void { + if (this.disableActionableElements) { + return; } - get quickFilters(): QuickFilters { - return this._quickFilters; + const executeOnDeactivate = (dFilter: QuickFilter) => { + if ( + this.suppressQuickFilterChangeEvent && + CollectionsUtil.isDefined(dFilter) && + CollectionsUtil.isFunction(dFilter.onDeactivate) + ) { + dFilter.onDeactivate(); + } + }; + + if (this.activatedFilter === filter) { + if (!filter.suppressCancel) { + this._deactivatedFilter = this.activatedFilter; + this.activatedFilter = null; + executeOnDeactivate(this._deactivatedFilter); + } + } else { + this._deactivatedFilter = this.activatedFilter; + this.activatedFilter = filter; + executeOnDeactivate(this._deactivatedFilter); } - /** - * ** Show or hide Label "QUICK FILTERS" before filters list. - * - * - true - Show - * - false - Hide - */ - @Input() showFiltersLabel = false; - - /** - * ** Suppress emitted event when some filter state change. - * - * - true - Event wont be emitted - * - false - Event would be emitted on change - */ - @Input() suppressQuickFilterChangeEvent = false; - - /** - * ** Flag that indicates actionable elements should be disabled. - */ - @Input() disableActionableElements = false; - - /** - * ** Event Emitter for Filter state change. - */ - @Output() quickFilterChange = new EventEmitter(); - - activatedFilter: QuickFilter; - - private _quickFilters: QuickFilters = []; - private _deactivatedFilter: QuickFilter | null = null; - - /** - * ** NgFor elements tracking function. - */ - trackByFn(index: number, filter: QuickFilter): string { - return `${index}|${filter.id}`; + if (this.suppressQuickFilterChangeEvent) { + if ( + CollectionsUtil.isDefined(this.activatedFilter) && + CollectionsUtil.isFunction(this.activatedFilter.onActivate) + ) { + this.activatedFilter.onActivate(); + } else { + console.warn( + "QuickFiltersComponent: No listener for onActivate callback while Event Emitter is suppressed.", + ); + } } - /** - * ** Executed when some filter change it's state. - *

    - * State changes when User click on some Filter or press Enter while it's on focus. - *

    - */ - changeFilter(filter: QuickFilter): void { - if (this.disableActionableElements) { - return; - } - - const executeOnDeactivate = (dFilter: QuickFilter) => { - if ( - this.suppressQuickFilterChangeEvent && - CollectionsUtil.isDefined(dFilter) && - CollectionsUtil.isFunction(dFilter.onDeactivate) - ) { - dFilter.onDeactivate(); - } - }; - - if (this.activatedFilter === filter) { - if (!filter.suppressCancel) { - this._deactivatedFilter = this.activatedFilter; - this.activatedFilter = null; - executeOnDeactivate(this._deactivatedFilter); - } - } else { - this._deactivatedFilter = this.activatedFilter; - this.activatedFilter = filter; - executeOnDeactivate(this._deactivatedFilter); - } - - if (this.suppressQuickFilterChangeEvent) { - if (CollectionsUtil.isDefined(this.activatedFilter) && CollectionsUtil.isFunction(this.activatedFilter.onActivate)) { - this.activatedFilter.onActivate(); - } else { - console.warn('QuickFiltersComponent: No listener for onActivate callback while Event Emitter is suppressed.'); - } - } - - this.quickFilterChange.emit({ - activatedFilter: this.activatedFilter, - deactivatedFilter: this._deactivatedFilter - }); - } - - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['quickFilters']) { - const defaultActiveFilter = this.quickFilters.find((f) => f.active); - - if (CollectionsUtil.isDefined(defaultActiveFilter)) { - this.activatedFilter = defaultActiveFilter; - } - } + this.quickFilterChange.emit({ + activatedFilter: this.activatedFilter, + deactivatedFilter: this._deactivatedFilter, + }); + } + + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges) { + if (changes["quickFilters"]) { + const defaultActiveFilter = this.quickFilters.find((f) => f.active); + + if (CollectionsUtil.isDefined(defaultActiveFilter)) { + this.activatedFilter = defaultActiveFilter; + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/index.ts index f82091559d..46d9eea009 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './status-panel/status-panel.component'; -export * from './status-cell/status-cell.component'; +export * from "./status-panel/status-panel.component"; +export * from "./status-cell/status-cell.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.css b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.css index c39cfe3ddb..16c8021fd2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.css +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.css @@ -4,9 +4,9 @@ */ .status-icon-enabled { - color: hsl(93, 67%, 38%); + color: hsl(93, 67%, 38%); } .status-icon-disabled { - color: hsl(32, 95%, 48%); + color: hsl(32, 95%, 48%); } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.html index 21da7099ce..1ee68053e1 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.html @@ -4,44 +4,44 @@ --> diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.spec.ts index 206b5383c0..3971a84980 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.spec.ts @@ -3,36 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ExtractJobStatusPipe } from '../../../pipes/extract-job-status.pipe'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ExtractJobStatusPipe } from "../../../pipes/extract-job-status.pipe"; -import { StatusCellComponent } from './status-cell.component'; +import { StatusCellComponent } from "./status-cell.component"; const TEST_JOB = { - jobName: 'job002', - config: { - description: 'description002' - } + jobName: "job002", + config: { + description: "description002", + }, }; -describe('StatusCellComponent', () => { - let component: StatusCellComponent; - let fixture: ComponentFixture; +describe("StatusCellComponent", () => { + let component: StatusCellComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [StatusCellComponent, ExtractJobStatusPipe] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StatusCellComponent, ExtractJobStatusPipe], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(StatusCellComponent); - component = fixture.componentInstance; - component.dataJob = TEST_JOB; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(StatusCellComponent); + component = fixture.componentInstance; + component.dataJob = TEST_JOB; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.ts index 9149915a6f..c9a51efac3 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-cell/status-cell.component.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input } from '@angular/core'; -import { DataJob } from '../../../../model/data-job.model'; +import { Component, Input } from "@angular/core"; +import { DataJob } from "../../../../model/data-job.model"; @Component({ - selector: 'lib-status-cell', - templateUrl: './status-cell.component.html', - styleUrls: ['./status-cell.component.css'] + selector: "lib-status-cell", + templateUrl: "./status-cell.component.html", + styleUrls: ["./status-cell.component.css"], }) export class StatusCellComponent { - @Input() dataJob: DataJob; + @Input() dataJob: DataJob; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.html index 6cf4ce3571..fc06decd56 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.html @@ -4,27 +4,27 @@ --> - Not Deployed - Disabled - Enabled + Not Deployed + Disabled + Enabled diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.spec.ts index 0d45c65b4d..aa893a2e20 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.spec.ts @@ -3,28 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ExtractJobStatusPipe } from '../../../pipes/extract-job-status.pipe'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ExtractJobStatusPipe } from "../../../pipes/extract-job-status.pipe"; -import { StatusPanelComponent } from './status-panel.component'; +import { StatusPanelComponent } from "./status-panel.component"; -describe('StatusPanelComponent', () => { - let component: StatusPanelComponent; - let fixture: ComponentFixture; +describe("StatusPanelComponent", () => { + let component: StatusPanelComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [StatusPanelComponent, ExtractJobStatusPipe] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StatusPanelComponent, ExtractJobStatusPipe], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(StatusPanelComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(StatusPanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.ts index 4042281d1a..3f0594be83 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/status/status-panel/status-panel.component.ts @@ -3,15 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input } from '@angular/core'; +import { Component, Input } from "@angular/core"; -import { DataJobDeployment } from '../../../../model'; +import { DataJobDeployment } from "../../../../model"; @Component({ - selector: 'lib-status-panel', - templateUrl: './status-panel.component.html', - styleUrls: ['./status-panel.component.css'] + selector: "lib-status-panel", + templateUrl: "./status-panel.component.html", + styleUrls: ["./status-panel.component.css"], }) export class StatusPanelComponent { - @Input() jobDeployments: DataJobDeployment[]; + @Input() jobDeployments: DataJobDeployment[]; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/index.ts index 8b6e1837f9..820549dc2f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './widget-value.component'; +export * from "./widget-value.component"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.html index 283f1eaa00..c50d107ed4 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.html @@ -4,27 +4,27 @@ --> - {{ prop ? data[prop] : data }} + {{ prop ? data[prop] : data }} - -
    - -
    -
    + +
    + +
    +
    - -
    - Loading ... -
    -
    + +
    + Loading ... +
    +
    diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.spec.ts index dae256c364..dd8a079d5c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.spec.ts @@ -3,27 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { WidgetValueComponent } from './widget-value.component'; +import { WidgetValueComponent } from "./widget-value.component"; -describe('WidgetValueComponent', () => { - let component: WidgetValueComponent; - let fixture: ComponentFixture; +describe("WidgetValueComponent", () => { + let component: WidgetValueComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [WidgetValueComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [WidgetValueComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(WidgetValueComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(WidgetValueComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.ts index 4428625b46..5fa28364c8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/components/widget-value/widget-value.component.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { Observable } from 'rxjs'; +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; +import { Observable } from "rxjs"; @Component({ - selector: 'lib-widget-value', - templateUrl: './widget-value.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + selector: "lib-widget-value", + templateUrl: "./widget-value.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WidgetValueComponent { - @Input() observable$: Observable; - @Input() prop: string; - @Input() showErrorState: boolean; + @Input() observable$: Observable; + @Input() prop: string; + @Input() showErrorState: boolean; } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.spec.ts index e214bbff5e..1070899ac5 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.spec.ts @@ -3,158 +3,161 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ElementRef, Renderer2 } from '@angular/core'; - -import { AttributesDirective } from './attribute.directive'; - -describe('AttributesDirective', () => { - let nativeElementStub: any; - let elementRefStub: ElementRef; - let rendererStub: jasmine.SpyObj; - - let directive: AttributesDirective; - - beforeEach(() => { - nativeElementStub = {}; - elementRefStub = { - nativeElement: nativeElementStub - }; - rendererStub = jasmine.createSpyObj('renderer2', ['setAttribute', 'removeAttribute']); - - directive = new AttributesDirective(elementRefStub, rendererStub); - }); - - it('should verify directive instance is created', () => { - // Then - expect(directive).toBeDefined(); - }); - - it('should verify on no attributes wont execute renderer', () => { - // When - directive.ngOnChanges({}); - directive.ngOnInit(); - - // Then - expect(rendererStub.setAttribute).not.toHaveBeenCalled(); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - }); - - it('should verify when attributes are null wont execute renderer', () => { - // When - directive.attributes = null; - directive.ngOnChanges({}); - directive.ngOnInit(); - - // Then - expect(rendererStub.setAttribute).not.toHaveBeenCalled(); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - }); - - it('should verify when attributes are not Literal Object wont execute renderer', () => { - // When - directive.attributes = new Map() as any; - directive.ngOnChanges({}); - directive.ngOnInit(); - - // Then - expect(rendererStub.setAttribute).not.toHaveBeenCalled(); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - }); - - it('should verify when providing two times same attributes by value but different reference would execute only once', () => { - // When 1 - directive.attributes = { - title: 'some-title', - size: 10, - shape: 'square', - class: 'css-class-1, css-class-2, css-class-3', - tabIndex: 0, - 'data-cy': 'cypress-selector' - }; - directive.ngOnInit(); - - // Then 1 - expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - - // When 2 - directive.attributes = { - title: 'some-title', - size: 10, - shape: 'square', - class: 'css-class-1, css-class-2, css-class-3', - tabIndex: 0, - 'data-cy': 'cypress-selector' - }; - directive.ngOnChanges({}); - - // Then - expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - }); - - it('should verify will set expected attributes from provided Literal Object', () => { - // When - directive.attributes = { - title: 'element-title', - size: 10, - shape: 'square', - class: 'css-class-1, css-class-2, css-class-3', - 'aria-label': 'aria-element-title', - tabIndex: 0, - 'data-cy': 'cypress-selector' - }; - directive.ngOnInit(); - - // Then - expect(rendererStub.setAttribute).toHaveBeenCalledTimes(7); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - }); - - it('should verify will remove expected attributes from provided Literal Object', () => { - // When - directive.attributes = { - title: undefined, - size: 10, - shape: 'square', - class: 'css-class-1, css-class-2, css-class-3', - 'aria-label': null, - tabIndex: 0, - 'data-cy': 'cypress-selector', - 'data-index': false, - 'data-attribute': 'delete', - 'data-btn': 'false', - 'data-attr': '' - }; - directive.ngOnInit(); - - // Then - expect(rendererStub.setAttribute).toHaveBeenCalledTimes(5); - expect(rendererStub.removeAttribute).toHaveBeenCalledTimes(6); - }); - - it('should verify will set and then remove provided attributes', () => { - // When 1 - directive.attributes = { - title: 'some-title', - size: 10, - shape: 'square', - class: 'css-class-1, css-class-2, css-class-3', - tabIndex: 0, - 'data-cy': 'cypress-selector' - }; - directive.ngOnInit(); - - // Then 1 - expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); - expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); - - // When 2 - directive.attributes = null; - directive.ngOnChanges({}); - - // Then 2 - expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); - expect(rendererStub.removeAttribute).toHaveBeenCalledTimes(6); - }); +import { ElementRef, Renderer2 } from "@angular/core"; + +import { AttributesDirective } from "./attribute.directive"; + +describe("AttributesDirective", () => { + let nativeElementStub: any; + let elementRefStub: ElementRef; + let rendererStub: jasmine.SpyObj; + + let directive: AttributesDirective; + + beforeEach(() => { + nativeElementStub = {}; + elementRefStub = { + nativeElement: nativeElementStub, + }; + rendererStub = jasmine.createSpyObj("renderer2", [ + "setAttribute", + "removeAttribute", + ]); + + directive = new AttributesDirective(elementRefStub, rendererStub); + }); + + it("should verify directive instance is created", () => { + // Then + expect(directive).toBeDefined(); + }); + + it("should verify on no attributes wont execute renderer", () => { + // When + directive.ngOnChanges({}); + directive.ngOnInit(); + + // Then + expect(rendererStub.setAttribute).not.toHaveBeenCalled(); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + }); + + it("should verify when attributes are null wont execute renderer", () => { + // When + directive.attributes = null; + directive.ngOnChanges({}); + directive.ngOnInit(); + + // Then + expect(rendererStub.setAttribute).not.toHaveBeenCalled(); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + }); + + it("should verify when attributes are not Literal Object wont execute renderer", () => { + // When + directive.attributes = new Map() as any; + directive.ngOnChanges({}); + directive.ngOnInit(); + + // Then + expect(rendererStub.setAttribute).not.toHaveBeenCalled(); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + }); + + it("should verify when providing two times same attributes by value but different reference would execute only once", () => { + // When 1 + directive.attributes = { + title: "some-title", + size: 10, + shape: "square", + class: "css-class-1, css-class-2, css-class-3", + tabIndex: 0, + "data-cy": "cypress-selector", + }; + directive.ngOnInit(); + + // Then 1 + expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + + // When 2 + directive.attributes = { + title: "some-title", + size: 10, + shape: "square", + class: "css-class-1, css-class-2, css-class-3", + tabIndex: 0, + "data-cy": "cypress-selector", + }; + directive.ngOnChanges({}); + + // Then + expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + }); + + it("should verify will set expected attributes from provided Literal Object", () => { + // When + directive.attributes = { + title: "element-title", + size: 10, + shape: "square", + class: "css-class-1, css-class-2, css-class-3", + "aria-label": "aria-element-title", + tabIndex: 0, + "data-cy": "cypress-selector", + }; + directive.ngOnInit(); + + // Then + expect(rendererStub.setAttribute).toHaveBeenCalledTimes(7); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + }); + + it("should verify will remove expected attributes from provided Literal Object", () => { + // When + directive.attributes = { + title: undefined, + size: 10, + shape: "square", + class: "css-class-1, css-class-2, css-class-3", + "aria-label": null, + tabIndex: 0, + "data-cy": "cypress-selector", + "data-index": false, + "data-attribute": "delete", + "data-btn": "false", + "data-attr": "", + }; + directive.ngOnInit(); + + // Then + expect(rendererStub.setAttribute).toHaveBeenCalledTimes(5); + expect(rendererStub.removeAttribute).toHaveBeenCalledTimes(6); + }); + + it("should verify will set and then remove provided attributes", () => { + // When 1 + directive.attributes = { + title: "some-title", + size: 10, + shape: "square", + class: "css-class-1, css-class-2, css-class-3", + tabIndex: 0, + "data-cy": "cypress-selector", + }; + directive.ngOnInit(); + + // Then 1 + expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); + expect(rendererStub.removeAttribute).not.toHaveBeenCalled(); + + // When 2 + directive.attributes = null; + directive.ngOnChanges({}); + + // Then 2 + expect(rendererStub.setAttribute).toHaveBeenCalledTimes(6); + expect(rendererStub.removeAttribute).toHaveBeenCalledTimes(6); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.ts index 15d0ab258a..33f862004e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/attribute.directive.ts @@ -3,12 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; - -import { CollectionsUtil, PrimitivesNil, TaurusObject } from '@versatiledatakit/shared'; +import { + Directive, + ElementRef, + Input, + OnChanges, + OnInit, + Renderer2, + SimpleChanges, +} from "@angular/core"; + +import { + CollectionsUtil, + PrimitivesNil, + TaurusObject, +} from "@versatiledatakit/shared"; export interface Attributes { - [attribute: string]: PrimitivesNil; + [attribute: string]: PrimitivesNil; } /** @@ -17,91 +29,114 @@ export interface Attributes { * @author gorankokin */ @Directive({ - selector: '[libSetAttributes]' + selector: "[libSetAttributes]", }) -export class AttributesDirective extends TaurusObject implements OnInit, OnChanges { - /** - * ** Input attributes that should be applied to host element. - */ - @Input() attributes: Attributes; - - private _attributesCopy: Attributes; - - /** - * ** Constructor. - */ - constructor( - private readonly el: ElementRef, - private readonly renderer: Renderer2 - ) { - super(); - } - - /** - * @inheritDoc - */ - ngOnChanges(_changes: SimpleChanges) { - this._transformAttributes(); - } - - /** - * @inheritDoc - */ - ngOnInit() { - this._transformAttributes(); +export class AttributesDirective + extends TaurusObject + implements OnInit, OnChanges +{ + /** + * ** Input attributes that should be applied to host element. + */ + @Input() attributes: Attributes; + + private _attributesCopy: Attributes; + + /** + * ** Constructor. + */ + constructor( + private readonly el: ElementRef, + private readonly renderer: Renderer2, + ) { + super(); + } + + /** + * @inheritDoc + */ + ngOnChanges(_changes: SimpleChanges) { + this._transformAttributes(); + } + + /** + * @inheritDoc + */ + ngOnInit() { + this._transformAttributes(); + } + + private _transformAttributes(): void { + if (CollectionsUtil.isEqual(this.attributes, this._attributesCopy)) { + return; } - private _transformAttributes(): void { - if (CollectionsUtil.isEqual(this.attributes, this._attributesCopy)) { - return; - } - - if (CollectionsUtil.isNil(this.attributes)) { - if (CollectionsUtil.isNil(this._attributesCopy)) { - return; - } - - CollectionsUtil.iterateObject(this._attributesCopy, (_attributeValue, attributeName) => { - this._removeAttribute(attributeName); - }); - - return; - } + if (CollectionsUtil.isNil(this.attributes)) { + if (CollectionsUtil.isNil(this._attributesCopy)) { + return; + } - if (!CollectionsUtil.isLiteralObject(this.attributes)) { - return; - } - - this._attributesCopy = CollectionsUtil.cloneDeep(this.attributes); - - CollectionsUtil.iterateObject(this._attributesCopy, (attributeValue, attributeName) => { - this._setOrRemoveAttribute(attributeName, attributeValue); - }); - } - - private _setOrRemoveAttribute(attributeName: string, attributeValue: unknown): void { - if (AttributesDirective._isTruthy(attributeValue)) { - this._setAttribute(attributeName, attributeValue); - } else { - this._removeAttribute(attributeName); - } - } - - private _setAttribute(attributeName: string, attributeValue: unknown): void { - this.renderer.setAttribute(this.el.nativeElement, attributeName, attributeValue as string); - } + CollectionsUtil.iterateObject( + this._attributesCopy, + (_attributeValue, attributeName) => { + this._removeAttribute(attributeName); + }, + ); - private _removeAttribute(attributeName: string): void { - this.renderer.removeAttribute(this.el.nativeElement, attributeName); + return; } - // eslint-disable-next-line @typescript-eslint/member-ordering,@typescript-eslint/no-explicit-any - private static _isTruthy(value: any): boolean { - return AttributesDirective._valueNotIn(value, [undefined, false, null, 'delete', 'false', '']); + if (!CollectionsUtil.isLiteralObject(this.attributes)) { + return; } - // eslint-disable-next-line @typescript-eslint/member-ordering,@typescript-eslint/no-explicit-any - private static _valueNotIn(value: any, forbiddenValues: any[]): boolean { - return forbiddenValues.every((prop) => value !== prop); + this._attributesCopy = CollectionsUtil.cloneDeep(this.attributes); + + CollectionsUtil.iterateObject( + this._attributesCopy, + (attributeValue, attributeName) => { + this._setOrRemoveAttribute(attributeName, attributeValue); + }, + ); + } + + private _setOrRemoveAttribute( + attributeName: string, + attributeValue: unknown, + ): void { + if (AttributesDirective._isTruthy(attributeValue)) { + this._setAttribute(attributeName, attributeValue); + } else { + this._removeAttribute(attributeName); } + } + + private _setAttribute(attributeName: string, attributeValue: unknown): void { + this.renderer.setAttribute( + this.el.nativeElement, + attributeName, + attributeValue as string, + ); + } + + private _removeAttribute(attributeName: string): void { + this.renderer.removeAttribute(this.el.nativeElement, attributeName); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering,@typescript-eslint/no-explicit-any + private static _isTruthy(value: any): boolean { + return AttributesDirective._valueNotIn(value, [ + undefined, + false, + null, + "delete", + "false", + "", + ]); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering,@typescript-eslint/no-explicit-any + private static _valueNotIn(value: any, forbiddenValues: any[]): boolean { + return forbiddenValues.every((prop) => value !== prop); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/index.ts index 7608d11614..490e6c3170 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/directives/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './attribute.directive'; +export * from "./attribute.directive"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/index.ts index 56e2b6049d..bda515e728 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './modal-options'; +export * from "./modal-options"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.spec.ts index 49272b0cb1..67c1714677 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.spec.ts @@ -3,42 +3,46 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EditModalOptions, ConfirmationModalOptions, DeleteModalOptions } from './modal-options'; +import { + EditModalOptions, + ConfirmationModalOptions, + DeleteModalOptions, +} from "./modal-options"; -describe('ModalOptions', () => { - it('DeleteModalOptions have initial values', () => { - const deleteModalOptions = new DeleteModalOptions(); - expect(deleteModalOptions.opened).toBeFalse(); - expect(deleteModalOptions.title).toBeDefined(); - expect(deleteModalOptions.message).toBeDefined(); - expect(deleteModalOptions.cancelBtn).toBeDefined(); - expect(deleteModalOptions.showCancelBtn).toBeTrue(); - expect(deleteModalOptions.okBtn).toBeDefined(); - expect(deleteModalOptions.showOkBtn).toBeTrue(); - expect(deleteModalOptions.showCloseX).toBeTrue(); - }); +describe("ModalOptions", () => { + it("DeleteModalOptions have initial values", () => { + const deleteModalOptions = new DeleteModalOptions(); + expect(deleteModalOptions.opened).toBeFalse(); + expect(deleteModalOptions.title).toBeDefined(); + expect(deleteModalOptions.message).toBeDefined(); + expect(deleteModalOptions.cancelBtn).toBeDefined(); + expect(deleteModalOptions.showCancelBtn).toBeTrue(); + expect(deleteModalOptions.okBtn).toBeDefined(); + expect(deleteModalOptions.showOkBtn).toBeTrue(); + expect(deleteModalOptions.showCloseX).toBeTrue(); + }); - it('EditModalOptions have initial values', () => { - const editModalOptions = new EditModalOptions(); - expect(editModalOptions.opened).toBeFalse(); - expect(editModalOptions.title).toBeDefined(); - expect(editModalOptions.message).toBeDefined(); - expect(editModalOptions.cancelBtn).toBeDefined(); - expect(editModalOptions.showCancelBtn).toBeTrue(); - expect(editModalOptions.okBtn).toBeDefined(); - expect(editModalOptions.showOkBtn).toBeTrue(); - expect(editModalOptions.showCloseX).toBeTrue(); - }); + it("EditModalOptions have initial values", () => { + const editModalOptions = new EditModalOptions(); + expect(editModalOptions.opened).toBeFalse(); + expect(editModalOptions.title).toBeDefined(); + expect(editModalOptions.message).toBeDefined(); + expect(editModalOptions.cancelBtn).toBeDefined(); + expect(editModalOptions.showCancelBtn).toBeTrue(); + expect(editModalOptions.okBtn).toBeDefined(); + expect(editModalOptions.showOkBtn).toBeTrue(); + expect(editModalOptions.showCloseX).toBeTrue(); + }); - it('ConfirmationModalOptions have initial values', () => { - const confirmationModalOptions = new ConfirmationModalOptions(); - expect(confirmationModalOptions.opened).toBeFalse(); - expect(confirmationModalOptions.title).toBeDefined(); - expect(confirmationModalOptions.message).toBeDefined(); - expect(confirmationModalOptions.cancelBtn).toBeDefined(); - expect(confirmationModalOptions.showCancelBtn).toBeTrue(); - expect(confirmationModalOptions.okBtn).toBeDefined(); - expect(confirmationModalOptions.showOkBtn).toBeTrue(); - expect(confirmationModalOptions.showCloseX).toBeTrue(); - }); + it("ConfirmationModalOptions have initial values", () => { + const confirmationModalOptions = new ConfirmationModalOptions(); + expect(confirmationModalOptions.opened).toBeFalse(); + expect(confirmationModalOptions.title).toBeDefined(); + expect(confirmationModalOptions.message).toBeDefined(); + expect(confirmationModalOptions.cancelBtn).toBeDefined(); + expect(confirmationModalOptions.showCancelBtn).toBeTrue(); + expect(confirmationModalOptions.okBtn).toBeDefined(); + expect(confirmationModalOptions.showOkBtn).toBeTrue(); + expect(confirmationModalOptions.showCloseX).toBeTrue(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.ts index 53f86084aa..a32b7438fe 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/model/modal-options.ts @@ -4,81 +4,81 @@ */ export interface ModalOptions { - opened: boolean; - title: string; - message: string; - cancelBtn: string; - showCancelBtn: boolean; - okBtn: string; - showOkBtn: boolean; - showCloseX: boolean; + opened: boolean; + title: string; + message: string; + cancelBtn: string; + showCancelBtn: boolean; + okBtn: string; + showOkBtn: boolean; + showCloseX: boolean; - infoText?: string; - warningText?: string; + infoText?: string; + warningText?: string; } export class DeleteModalOptions implements ModalOptions { - opened: boolean; - title: string; - message: string; - cancelBtn: string; - showCancelBtn: boolean; - okBtn: string; - showOkBtn: boolean; - showCloseX: boolean; + opened: boolean; + title: string; + message: string; + cancelBtn: string; + showCancelBtn: boolean; + okBtn: string; + showOkBtn: boolean; + showCloseX: boolean; - constructor() { - this.opened = false; - this.title = 'Delete'; - this.message = 'Are you sure you want to permanently delete this item?'; - this.cancelBtn = 'Cancel'; - this.showCancelBtn = true; - this.okBtn = 'Delete'; - this.showOkBtn = true; - this.showCloseX = true; - } + constructor() { + this.opened = false; + this.title = "Delete"; + this.message = "Are you sure you want to permanently delete this item?"; + this.cancelBtn = "Cancel"; + this.showCancelBtn = true; + this.okBtn = "Delete"; + this.showOkBtn = true; + this.showCloseX = true; + } } export class EditModalOptions implements ModalOptions { - opened: boolean; - title: string; - message: string; - cancelBtn: string; - showCancelBtn: boolean; - okBtn: string; - showOkBtn: boolean; - showCloseX: boolean; + opened: boolean; + title: string; + message: string; + cancelBtn: string; + showCancelBtn: boolean; + okBtn: string; + showOkBtn: boolean; + showCloseX: boolean; - constructor() { - this.opened = false; - this.title = 'Edit'; - this.message = ''; - this.cancelBtn = 'Cancel'; - this.showCancelBtn = true; - this.okBtn = 'Edit'; - this.showOkBtn = true; - this.showCloseX = true; - } + constructor() { + this.opened = false; + this.title = "Edit"; + this.message = ""; + this.cancelBtn = "Cancel"; + this.showCancelBtn = true; + this.okBtn = "Edit"; + this.showOkBtn = true; + this.showCloseX = true; + } } export class ConfirmationModalOptions implements ModalOptions { - opened: boolean; - title: string; - message: string; - cancelBtn: string; - showCancelBtn: boolean; - okBtn: string; - showOkBtn: boolean; - showCloseX: boolean; + opened: boolean; + title: string; + message: string; + cancelBtn: string; + showCancelBtn: boolean; + okBtn: string; + showOkBtn: boolean; + showCloseX: boolean; - constructor() { - this.opened = false; - this.title = 'Confirm'; - this.message = 'Are you sure?'; - this.cancelBtn = 'Cancel'; - this.showCancelBtn = true; - this.okBtn = 'Confirm'; - this.showOkBtn = true; - this.showCloseX = true; - } + constructor() { + this.opened = false; + this.title = "Confirm"; + this.message = "Are you sure?"; + this.cancelBtn = "Cancel"; + this.showCancelBtn = true; + this.okBtn = "Confirm"; + this.showOkBtn = true; + this.showCloseX = true; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.spec.ts index 08fa15479d..658464dbce 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.spec.ts @@ -3,109 +3,109 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataJobContacts } from '../../model'; +import { DataJobContacts } from "../../model"; -import { ContactsPresentPipe } from './contacts-present.pipe'; +import { ContactsPresentPipe } from "./contacts-present.pipe"; -describe('ContactsPresentPipe', () => { - it('should verify instance is created', () => { - // When - const instance = new ContactsPresentPipe(); +describe("ContactsPresentPipe", () => { + it("should verify instance is created", () => { + // When + const instance = new ContactsPresentPipe(); - // Then - expect(instance).toBeDefined(); - }); + // Then + expect(instance).toBeDefined(); + }); - describe('Methods::', () => { - let pipe: ContactsPresentPipe; + describe("Methods::", () => { + let pipe: ContactsPresentPipe; - beforeEach(() => { - pipe = new ContactsPresentPipe(); - }); + beforeEach(() => { + pipe = new ContactsPresentPipe(); + }); - describe('|transform|', () => { - const parameters: Array<{ - contacts: DataJobContacts; - expected: boolean; - }> = [ - { contacts: null, expected: false }, - { - contacts: { - notifiedOnJobSuccess: undefined, - notifiedOnJobDeploy: undefined, - notifiedOnJobFailurePlatformError: undefined, - notifiedOnJobFailureUserError: undefined - }, - expected: false - }, - { - contacts: { - notifiedOnJobSuccess: [], - notifiedOnJobDeploy: [], - notifiedOnJobFailurePlatformError: [], - notifiedOnJobFailureUserError: [] - }, - expected: false - }, - { - contacts: { - notifiedOnJobSuccess: ['alpha@abc.com'], - notifiedOnJobDeploy: [], - notifiedOnJobFailurePlatformError: [], - notifiedOnJobFailureUserError: [] - }, - expected: true - }, - { - contacts: { - notifiedOnJobSuccess: [], - notifiedOnJobDeploy: ['beta@abc.com'], - notifiedOnJobFailurePlatformError: [], - notifiedOnJobFailureUserError: [] - }, - expected: true - }, - { - contacts: { - notifiedOnJobSuccess: [], - notifiedOnJobDeploy: [], - notifiedOnJobFailurePlatformError: ['gama@abc.com'], - notifiedOnJobFailureUserError: [] - }, - expected: true - }, - { - contacts: { - notifiedOnJobSuccess: [], - notifiedOnJobDeploy: [], - notifiedOnJobFailurePlatformError: [], - notifiedOnJobFailureUserError: ['delta@abc.com'] - }, - expected: true - }, - { - contacts: { - notifiedOnJobSuccess: ['alpha@abc.com'], - notifiedOnJobDeploy: ['beta@abc.com'], - notifiedOnJobFailurePlatformError: ['gama@abc.com'], - notifiedOnJobFailureUserError: ['delta@abc.com'] - }, - expected: true - } - ]; + describe("|transform|", () => { + const parameters: Array<{ + contacts: DataJobContacts; + expected: boolean; + }> = [ + { contacts: null, expected: false }, + { + contacts: { + notifiedOnJobSuccess: undefined, + notifiedOnJobDeploy: undefined, + notifiedOnJobFailurePlatformError: undefined, + notifiedOnJobFailureUserError: undefined, + }, + expected: false, + }, + { + contacts: { + notifiedOnJobSuccess: [], + notifiedOnJobDeploy: [], + notifiedOnJobFailurePlatformError: [], + notifiedOnJobFailureUserError: [], + }, + expected: false, + }, + { + contacts: { + notifiedOnJobSuccess: ["alpha@abc.com"], + notifiedOnJobDeploy: [], + notifiedOnJobFailurePlatformError: [], + notifiedOnJobFailureUserError: [], + }, + expected: true, + }, + { + contacts: { + notifiedOnJobSuccess: [], + notifiedOnJobDeploy: ["beta@abc.com"], + notifiedOnJobFailurePlatformError: [], + notifiedOnJobFailureUserError: [], + }, + expected: true, + }, + { + contacts: { + notifiedOnJobSuccess: [], + notifiedOnJobDeploy: [], + notifiedOnJobFailurePlatformError: ["gama@abc.com"], + notifiedOnJobFailureUserError: [], + }, + expected: true, + }, + { + contacts: { + notifiedOnJobSuccess: [], + notifiedOnJobDeploy: [], + notifiedOnJobFailurePlatformError: [], + notifiedOnJobFailureUserError: ["delta@abc.com"], + }, + expected: true, + }, + { + contacts: { + notifiedOnJobSuccess: ["alpha@abc.com"], + notifiedOnJobDeploy: ["beta@abc.com"], + notifiedOnJobFailurePlatformError: ["gama@abc.com"], + notifiedOnJobFailureUserError: ["delta@abc.com"], + }, + expected: true, + }, + ]; - let cnt = 0; - for (const params of parameters) { - cnt++; + let cnt = 0; + for (const params of parameters) { + cnt++; - it(`should verify will return ${params.expected as unknown as string} case ${cnt}`, () => { - // When - const value = pipe.transform(params.contacts); + it(`should verify will return ${params.expected as unknown as string} case ${cnt}`, () => { + // When + const value = pipe.transform(params.contacts); - // Then - expect(value).toEqual(params.expected); - }); - } + // Then + expect(value).toEqual(params.expected); }); + } }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.ts index 0ce5f6c325..9d4a91a7b4 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/contacts-present.pipe.ts @@ -3,31 +3,35 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { DataJobContacts } from '../../model'; +import { DataJobContacts } from "../../model"; @Pipe({ - name: 'contactsPresent' + name: "contactsPresent", }) export class ContactsPresentPipe implements PipeTransform { - /** - * @inheritDoc - */ - transform(contacts: DataJobContacts): boolean { - return ( - CollectionsUtil.isDefined(contacts) && - (ContactsPresentPipe.contactIsPresent(contacts.notifiedOnJobSuccess) || - ContactsPresentPipe.contactIsPresent(contacts.notifiedOnJobDeploy) || - ContactsPresentPipe.contactIsPresent(contacts.notifiedOnJobFailureUserError) || - ContactsPresentPipe.contactIsPresent(contacts.notifiedOnJobFailurePlatformError)) - ); - } + /** + * @inheritDoc + */ + transform(contacts: DataJobContacts): boolean { + return ( + CollectionsUtil.isDefined(contacts) && + (ContactsPresentPipe.contactIsPresent(contacts.notifiedOnJobSuccess) || + ContactsPresentPipe.contactIsPresent(contacts.notifiedOnJobDeploy) || + ContactsPresentPipe.contactIsPresent( + contacts.notifiedOnJobFailureUserError, + ) || + ContactsPresentPipe.contactIsPresent( + contacts.notifiedOnJobFailurePlatformError, + )) + ); + } - // eslint-disable-next-line @typescript-eslint/member-ordering - private static contactIsPresent(contacts: string[]): boolean { - return CollectionsUtil.isArray(contacts) && contacts.length > 0; - } + // eslint-disable-next-line @typescript-eslint/member-ordering + private static contactIsPresent(contacts: string[]): boolean { + return CollectionsUtil.isArray(contacts) && contacts.length > 0; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.spec.ts index cd21778c22..b49ca9aca7 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.spec.ts @@ -3,87 +3,87 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataJobDeployment } from '../../model'; - -import { ExecutionSuccessRatePipe } from './execution-success-rate.pipe'; - -describe('ExecutionSuccessRatePipe', () => { - let localeId: string; - let pipe: ExecutionSuccessRatePipe; - let deployments: DataJobDeployment[]; - - beforeEach(() => { - localeId = 'en-US'; - pipe = new ExecutionSuccessRatePipe(localeId); - deployments = [ - { - successfulExecutions: 20, - failedExecutions: 5 - } as DataJobDeployment - ]; - }); +import { DataJobDeployment } from "../../model"; + +import { ExecutionSuccessRatePipe } from "./execution-success-rate.pipe"; + +describe("ExecutionSuccessRatePipe", () => { + let localeId: string; + let pipe: ExecutionSuccessRatePipe; + let deployments: DataJobDeployment[]; + + beforeEach(() => { + localeId = "en-US"; + pipe = new ExecutionSuccessRatePipe(localeId); + deployments = [ + { + successfulExecutions: 20, + failedExecutions: 5, + } as DataJobDeployment, + ]; + }); + + it("should verify instance is created", () => { + // Then + expect(pipe).toBeDefined(); + }); + + describe("Methods::", () => { + describe("|transform|", () => { + it(`should verify will return '' when no deployments or Empty Array`, () => { + // When + const r1 = pipe.transform(null); + const r2 = pipe.transform(undefined); + const r3 = pipe.transform([]); - it('should verify instance is created', () => { // Then - expect(pipe).toBeDefined(); - }); + expect(r1).toEqual(""); + expect(r2).toEqual(""); + expect(r3).toEqual(""); + }); + + it(`should verify will return '' when sum of all all executions is 0`, () => { + // Given + deployments[0].successfulExecutions = 0; + deployments[0].failedExecutions = 0; + + // When + const r = pipe.transform(deployments); + + // Then + expect(r).toEqual(""); + }); + + it("should verify will return 100% when no failed executions", () => { + // Given + deployments[0].failedExecutions = 0; + + // When + const r = pipe.transform(deployments); - describe('Methods::', () => { - describe('|transform|', () => { - it(`should verify will return '' when no deployments or Empty Array`, () => { - // When - const r1 = pipe.transform(null); - const r2 = pipe.transform(undefined); - const r3 = pipe.transform([]); - - // Then - expect(r1).toEqual(''); - expect(r2).toEqual(''); - expect(r3).toEqual(''); - }); - - it(`should verify will return '' when sum of all all executions is 0`, () => { - // Given - deployments[0].successfulExecutions = 0; - deployments[0].failedExecutions = 0; - - // When - const r = pipe.transform(deployments); - - // Then - expect(r).toEqual(''); - }); - - it('should verify will return 100% when no failed executions', () => { - // Given - deployments[0].failedExecutions = 0; - - // When - const r = pipe.transform(deployments); - - // Then - expect(r).toEqual('100.00%'); - }); - - it('should verify will return percent of success and number of failed executions (case 1)', () => { - // When - const r = pipe.transform(deployments); - - // Then - expect(r).toEqual('80.00% (5 failed)'); - }); - - it('should verify will return percent of success and number of failed executions (case 2)', () => { - // Given - deployments[0].successfulExecutions = 18; - deployments[0].failedExecutions = 15; - - // When - const r = pipe.transform(deployments); - - // Then - expect(r).toEqual('54.55% (15 failed)'); - }); - }); + // Then + expect(r).toEqual("100.00%"); + }); + + it("should verify will return percent of success and number of failed executions (case 1)", () => { + // When + const r = pipe.transform(deployments); + + // Then + expect(r).toEqual("80.00% (5 failed)"); + }); + + it("should verify will return percent of success and number of failed executions (case 2)", () => { + // Given + deployments[0].successfulExecutions = 18; + deployments[0].failedExecutions = 15; + + // When + const r = pipe.transform(deployments); + + // Then + expect(r).toEqual("54.55% (15 failed)"); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.ts index 06db00e006..4b671fa50d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/execution-success-rate.pipe.ts @@ -3,49 +3,53 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core'; -import { PercentPipe } from '@angular/common'; +import { Inject, LOCALE_ID, Pipe, PipeTransform } from "@angular/core"; +import { PercentPipe } from "@angular/common"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { DataJobDeployment } from '../../model'; +import { DataJobDeployment } from "../../model"; @Pipe({ - name: 'executionSuccessRate' + name: "executionSuccessRate", }) export class ExecutionSuccessRatePipe implements PipeTransform { - private readonly _percentPipe: PercentPipe; - - /** - * ** Constructor. - */ - constructor(@Inject(LOCALE_ID) readonly localeId: string) { - this._percentPipe = new PercentPipe(localeId); + private readonly _percentPipe: PercentPipe; + + /** + * ** Constructor. + */ + constructor(@Inject(LOCALE_ID) readonly localeId: string) { + this._percentPipe = new PercentPipe(localeId); + } + + /** + * @inheritDoc + */ + transform(deployments: DataJobDeployment[]): string { + let result = ""; + + if (CollectionsUtil.isArrayEmpty(deployments)) { + return result; } - /** - * @inheritDoc - */ - transform(deployments: DataJobDeployment[]): string { - let result = ''; - - if (CollectionsUtil.isArrayEmpty(deployments)) { - return result; - } - - const firstDeployment = deployments[0]; - const allExecutions = firstDeployment.successfulExecutions + firstDeployment.failedExecutions; + const firstDeployment = deployments[0]; + const allExecutions = + firstDeployment.successfulExecutions + firstDeployment.failedExecutions; - if (allExecutions === 0) { - return result; - } - - result += this._percentPipe.transform(firstDeployment.successfulExecutions / allExecutions, '1.2-2'); + if (allExecutions === 0) { + return result; + } - if (firstDeployment.failedExecutions > 0) { - result += ` (${firstDeployment.failedExecutions} failed)`; - } + result += this._percentPipe.transform( + firstDeployment.successfulExecutions / allExecutions, + "1.2-2", + ); - return result; + if (firstDeployment.failedExecutions > 0) { + result += ` (${firstDeployment.failedExecutions} failed)`; } + + return result; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-contacts.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-contacts.pipe.ts index 25258e3106..ae18fdd751 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-contacts.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-contacts.pipe.ts @@ -3,21 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; @Pipe({ - name: 'extractContacts' + name: "extractContacts", }) export class ExtractContactsPipe implements PipeTransform { - static transform(contacts: string[]): string[] { - if (Array.isArray(contacts) && contacts.length) { - return contacts; - } else { - return []; - } + static transform(contacts: string[]): string[] { + if (Array.isArray(contacts) && contacts.length) { + return contacts; + } else { + return []; } + } - transform(contacts: string[]): string[] { - return ExtractContactsPipe.transform(contacts); - } + transform(contacts: string[]): string[] { + return ExtractContactsPipe.transform(contacts); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.spec.ts index e481a0c810..eb8b6b9d6a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.spec.ts @@ -3,61 +3,61 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { DataJobDeploymentDetails } from '../../model/data-job-details.model'; -import { ExtractJobStatusPipe } from './extract-job-status.pipe'; +import { TestBed } from "@angular/core/testing"; +import { DataJobDeploymentDetails } from "../../model/data-job-details.model"; +import { ExtractJobStatusPipe } from "./extract-job-status.pipe"; const TEST_JOB_DEPLOYMENT_DETAILS: DataJobDeploymentDetails = { - id: 'id002', - enabled: true, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - job_version: 'v001', - mode: 'special', - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - vdk_version: 'v002', - python_version: '3.9-secure', - contacts: null, - /* eslint-disable @typescript-eslint/naming-convention */ - deployed_date: '2020-11-11T10:10:10Z', - deployed_by: 'pmitev', - resources: { - memory_limit: 1000, - memory_request: 1000, - cpu_limit: 0.5, - cpu_request: 0.5 - } - /* eslint-enable @typescript-eslint/naming-convention */ + id: "id002", + enabled: true, + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + job_version: "v001", + mode: "special", + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + vdk_version: "v002", + python_version: "3.9-secure", + contacts: null, + /* eslint-disable @typescript-eslint/naming-convention */ + deployed_date: "2020-11-11T10:10:10Z", + deployed_by: "pmitev", + resources: { + memory_limit: 1000, + memory_request: 1000, + cpu_limit: 0.5, + cpu_request: 0.5, + }, + /* eslint-enable @typescript-eslint/naming-convention */ }; -describe('ExtractJobStatusPipe', () => { - let pipe: ExtractJobStatusPipe; - let deploymentDetails: DataJobDeploymentDetails; +describe("ExtractJobStatusPipe", () => { + let pipe: ExtractJobStatusPipe; + let deploymentDetails: DataJobDeploymentDetails; - beforeEach(() => { - TestBed.configureTestingModule({ providers: [ExtractJobStatusPipe] }); - pipe = TestBed.inject(ExtractJobStatusPipe); - deploymentDetails = TEST_JOB_DEPLOYMENT_DETAILS; - }); + beforeEach(() => { + TestBed.configureTestingModule({ providers: [ExtractJobStatusPipe] }); + pipe = TestBed.inject(ExtractJobStatusPipe); + deploymentDetails = TEST_JOB_DEPLOYMENT_DETAILS; + }); - it('can instantiate ExtractJobStatusPipe', () => { - expect(pipe).toBeTruthy(); - }); + it("can instantiate ExtractJobStatusPipe", () => { + expect(pipe).toBeTruthy(); + }); - it('transforms empty deploymentDetails to NOT_DEPLOYED', () => { - expect(pipe.transform([])).toEqual('Not Deployed'); - }); + it("transforms empty deploymentDetails to NOT_DEPLOYED", () => { + expect(pipe.transform([])).toEqual("Not Deployed"); + }); - it('transforms disabled deploymentDetails to DISABLED', () => { - deploymentDetails.enabled = false; + it("transforms disabled deploymentDetails to DISABLED", () => { + deploymentDetails.enabled = false; - const jobDeployments = [deploymentDetails]; - expect(pipe.transform(jobDeployments)).toEqual('Disabled'); - }); + const jobDeployments = [deploymentDetails]; + expect(pipe.transform(jobDeployments)).toEqual("Disabled"); + }); - it('transforms enabled deploymentDetails to DISABLED', () => { - deploymentDetails.enabled = true; + it("transforms enabled deploymentDetails to DISABLED", () => { + deploymentDetails.enabled = true; - const jobExecutions = [deploymentDetails]; - expect(pipe.transform(jobExecutions)).toEqual('Enabled'); - }); + const jobExecutions = [deploymentDetails]; + expect(pipe.transform(jobExecutions)).toEqual("Enabled"); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.ts index 661d3f4785..f9838892f9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/extract-job-status.pipe.ts @@ -3,40 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; -import { DataJobStatus, StatusDetails } from '../../model'; +import { DataJobStatus, StatusDetails } from "../../model"; @Pipe({ - name: 'extractJobStatus', - pure: false + name: "extractJobStatus", + pure: false, }) export class ExtractJobStatusPipe implements PipeTransform { - /** - * ** Extract Job Status from Details. - * - * - This method should be equal to instance method. - * - Methods: {@link ExtractJobStatusPipe.transform} - */ - static transform(jobDeployments: StatusDetails[]): DataJobStatus { - if (!jobDeployments?.length) { - return DataJobStatus.NOT_DEPLOYED; - } - - if (jobDeployments[jobDeployments.length - 1].enabled) { - return DataJobStatus.ENABLED; - } - - return DataJobStatus.DISABLED; + /** + * ** Extract Job Status from Details. + * + * - This method should be equal to instance method. + * - Methods: {@link ExtractJobStatusPipe.transform} + */ + static transform(jobDeployments: StatusDetails[]): DataJobStatus { + if (!jobDeployments?.length) { + return DataJobStatus.NOT_DEPLOYED; } - /** - * @inheritDoc - * - * - This method should be equal to instance method. - * - Methods: {@link ExtractJobStatusPipe.transform} - */ - transform(jobDeployments: StatusDetails[]): DataJobStatus { - return ExtractJobStatusPipe.transform(jobDeployments); + if (jobDeployments[jobDeployments.length - 1].enabled) { + return DataJobStatus.ENABLED; } + + return DataJobStatus.DISABLED; + } + + /** + * @inheritDoc + * + * - This method should be equal to instance method. + * - Methods: {@link ExtractJobStatusPipe.transform} + */ + transform(jobDeployments: StatusDetails[]): DataJobStatus { + return ExtractJobStatusPipe.transform(jobDeployments); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.spec.ts index fd3ae60e68..e38f3af8c3 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.spec.ts @@ -3,90 +3,94 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; -import { DataJobExecution, DataJobExecutionStatus, DataJobExecutionType } from '../../model'; +import { + DataJobExecution, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../model"; -import { FormatDeltaPipe } from './format-delta.pipe'; +import { FormatDeltaPipe } from "./format-delta.pipe"; const TEST_JOBS_EXECUTIONS = { - id: 'id001', - jobName: 'name001', - status: DataJobExecutionStatus.RUNNING, - startTime: new Date().toISOString(), - startedBy: 'aUserov', - type: DataJobExecutionType.SCHEDULED, - endTime: new Date().toISOString(), - opId: 'op001', - message: 'Message 001' + id: "id001", + jobName: "name001", + status: DataJobExecutionStatus.RUNNING, + startTime: new Date().toISOString(), + startedBy: "aUserov", + type: DataJobExecutionType.SCHEDULED, + endTime: new Date().toISOString(), + opId: "op001", + message: "Message 001", } as DataJobExecution; -describe('FormatDeltaPipe', () => { - let pipe: FormatDeltaPipe; - let execution: DataJobExecution; - - beforeEach(() => { - TestBed.configureTestingModule({ providers: [FormatDeltaPipe] }); - pipe = TestBed.inject(FormatDeltaPipe); - execution = { ...TEST_JOBS_EXECUTIONS }; - }); - - it('can instantiate FormatDeltaPipe', () => { - expect(pipe).toBeTruthy(); - }); - - it('transforms missing startTime to valid value', (): void => { - execution.startTime = undefined; - execution.endTime = undefined; - expect(pipe.transform(execution)).toEqual(''); - }); - - it('transforms invalid EndData to valid delta', (): void => { - execution.startTime = substract(new Date().toISOString(), 1); - execution.endTime = null; - expect(pipe.transform(execution)).toBeDefined(); - }); - - it('transforms a out of range input to NaN delta', (): void => { - const currentDate = new Date().toISOString(); - execution.startTime = substract(currentDate, -10); - execution.endTime = currentDate; - expect(pipe.transform(execution)).toBe('N/A'); - }); - - it('transforms a seconds range input to valid delta', (): void => { - const currentDate = new Date().toISOString(); - execution.startTime = substract(currentDate, 10); - execution.endTime = currentDate; - expect(pipe.transform(execution)).toBe('10s'); - }); - - it('transforms a minutes range input to valid delta', (): void => { - const currentDate = new Date().toISOString(); - execution.startTime = substract(currentDate, 121); - execution.endTime = currentDate; - expect(pipe.transform(execution)).toBe('2m 1s'); - }); - - it('transforms an hours range input to valid delta', (): void => { - const currentDate = new Date().toISOString(); - execution.startTime = substract(currentDate, 7260); - execution.endTime = currentDate; - expect(pipe.transform(execution)).toBe('2h 1m'); - }); - - it('transforms a days range input to valid delta', (): void => { - const currentDate = new Date().toISOString(); - execution.startTime = substract(currentDate, 176400); - execution.endTime = currentDate; - expect(pipe.transform(execution)).toBe('2d 1h'); - }); - - const substract = (date: string, secondsToSubstract: number) => { - const d = new Date(date); - const result = d; - result.setTime(d.getTime() - 1000 * secondsToSubstract); - - return result.toISOString(); - }; +describe("FormatDeltaPipe", () => { + let pipe: FormatDeltaPipe; + let execution: DataJobExecution; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [FormatDeltaPipe] }); + pipe = TestBed.inject(FormatDeltaPipe); + execution = { ...TEST_JOBS_EXECUTIONS }; + }); + + it("can instantiate FormatDeltaPipe", () => { + expect(pipe).toBeTruthy(); + }); + + it("transforms missing startTime to valid value", (): void => { + execution.startTime = undefined; + execution.endTime = undefined; + expect(pipe.transform(execution)).toEqual(""); + }); + + it("transforms invalid EndData to valid delta", (): void => { + execution.startTime = substract(new Date().toISOString(), 1); + execution.endTime = null; + expect(pipe.transform(execution)).toBeDefined(); + }); + + it("transforms a out of range input to NaN delta", (): void => { + const currentDate = new Date().toISOString(); + execution.startTime = substract(currentDate, -10); + execution.endTime = currentDate; + expect(pipe.transform(execution)).toBe("N/A"); + }); + + it("transforms a seconds range input to valid delta", (): void => { + const currentDate = new Date().toISOString(); + execution.startTime = substract(currentDate, 10); + execution.endTime = currentDate; + expect(pipe.transform(execution)).toBe("10s"); + }); + + it("transforms a minutes range input to valid delta", (): void => { + const currentDate = new Date().toISOString(); + execution.startTime = substract(currentDate, 121); + execution.endTime = currentDate; + expect(pipe.transform(execution)).toBe("2m 1s"); + }); + + it("transforms an hours range input to valid delta", (): void => { + const currentDate = new Date().toISOString(); + execution.startTime = substract(currentDate, 7260); + execution.endTime = currentDate; + expect(pipe.transform(execution)).toBe("2h 1m"); + }); + + it("transforms a days range input to valid delta", (): void => { + const currentDate = new Date().toISOString(); + execution.startTime = substract(currentDate, 176400); + execution.endTime = currentDate; + expect(pipe.transform(execution)).toBe("2d 1h"); + }); + + const substract = (date: string, secondsToSubstract: number) => { + const d = new Date(date); + const result = d; + result.setTime(d.getTime() - 1000 * secondsToSubstract); + + return result.toISOString(); + }; }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.ts index 51f4866180..8070b62cac 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-delta.pipe.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; -import { DataJobExecution } from '../../model'; +import { DataJobExecution } from "../../model"; /** * Format Delta Pipe formats the delta of the execution start and end Time. @@ -24,60 +24,63 @@ import { DataJobExecution } from '../../model'; * 4: If the duration is more than 1 day, the format is `${days}d ${hours}h` */ @Pipe({ - name: 'formatDelta' + name: "formatDelta", }) export class FormatDeltaPipe implements PipeTransform { - static formatDelta(delta: number): string { - if (delta < 0) { - return 'N/A'; - } else if (delta < 60) { - return `${Math.ceil(delta)}s`; - } else if (delta < 3600) { - const minute = Math.floor((delta / 60) % 60); - const seconds = Math.floor(delta % 60); + static formatDelta(delta: number): string { + if (delta < 0) { + return "N/A"; + } else if (delta < 60) { + return `${Math.ceil(delta)}s`; + } else if (delta < 3600) { + const minute = Math.floor((delta / 60) % 60); + const seconds = Math.floor(delta % 60); - return `${minute}m ${seconds}s`; - } else if (delta < 86400) { - const hours = Math.floor((delta / (60 * 60)) % 24); - const minutes = Math.floor((delta / 60) % 60); + return `${minute}m ${seconds}s`; + } else if (delta < 86400) { + const hours = Math.floor((delta / (60 * 60)) % 24); + const minutes = Math.floor((delta / 60) % 60); - return `${hours}h ${minutes}m`; - } else { - const days = Math.floor(delta / (60 * 60 * 24)); - const hours = Math.floor((delta / (60 * 60)) % 24); + return `${hours}h ${minutes}m`; + } else { + const days = Math.floor(delta / (60 * 60 * 24)); + const hours = Math.floor((delta / (60 * 60)) % 24); - return `${days}d ${hours}h`; - } + return `${days}d ${hours}h`; } + } - /** - * @inheritDoc - */ - transform(execution: DataJobExecution): string { - if (CollectionsUtil.isNil(execution.startTime)) { - return ''; - } - - const delta = (FormatDeltaPipe._getEndTime(execution) - FormatDeltaPipe._getStartTime(execution)) / 1000; - - return FormatDeltaPipe.formatDelta(delta); + /** + * @inheritDoc + */ + transform(execution: DataJobExecution): string { + if (CollectionsUtil.isNil(execution.startTime)) { + return ""; } - // eslint-disable-next-line @typescript-eslint/member-ordering - private static _getStartTime(execution: DataJobExecution): number { - if (CollectionsUtil.isDefined(execution.startTime)) { - return new Date(execution.startTime).getTime(); - } + const delta = + (FormatDeltaPipe._getEndTime(execution) - + FormatDeltaPipe._getStartTime(execution)) / + 1000; - return Date.now(); + return FormatDeltaPipe.formatDelta(delta); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + private static _getStartTime(execution: DataJobExecution): number { + if (CollectionsUtil.isDefined(execution.startTime)) { + return new Date(execution.startTime).getTime(); } - // eslint-disable-next-line @typescript-eslint/member-ordering - private static _getEndTime(execution: DataJobExecution): number { - if (CollectionsUtil.isDefined(execution.endTime)) { - return new Date(execution.endTime).getTime(); - } + return Date.now(); + } - return Date.now(); + // eslint-disable-next-line @typescript-eslint/member-ordering + private static _getEndTime(execution: DataJobExecution): number { + if (CollectionsUtil.isDefined(execution.endTime)) { + return new Date(execution.endTime).getTime(); } + + return Date.now(); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-duration.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-duration.pipe.ts index 717f01aa93..8785773ac8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-duration.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-duration.pipe.ts @@ -3,17 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; -import { FormatDeltaPipe } from './format-delta.pipe'; +import { Pipe, PipeTransform } from "@angular/core"; +import { FormatDeltaPipe } from "./format-delta.pipe"; @Pipe({ - name: 'formatDuration' + name: "formatDuration", }) export class FormatDurationPipe implements PipeTransform { - /** - * @inheritDoc - */ - transform(durationSeconds: number): string { - return durationSeconds ? FormatDeltaPipe.formatDelta(durationSeconds) : null; - } + /** + * @inheritDoc + */ + transform(durationSeconds: number): string { + return durationSeconds + ? FormatDeltaPipe.formatDelta(durationSeconds) + : null; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.spec.ts index 2c776496a0..19130f103a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.spec.ts @@ -3,412 +3,440 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { FormatSchedulePipe } from './format-schedule.pipe'; +import { FormatSchedulePipe } from "./format-schedule.pipe"; -describe('FormatSchedulePipe', () => { - let pipe: FormatSchedulePipe; +describe("FormatSchedulePipe", () => { + let pipe: FormatSchedulePipe; - beforeEach(() => { - pipe = new FormatSchedulePipe(); - }); + beforeEach(() => { + pipe = new FormatSchedulePipe(); + }); + + it("can instantiate FormatSchedulePipe", () => { + // Then + expect(pipe).toBeTruthy(); + }); + + describe("Methods::", () => { + describe("|transform|", () => { + it("should verify on missing schedule returns empty result", () => { + // Given + const schedule: string = null; + + // When + const result = pipe.transform(schedule); - it('can instantiate FormatSchedulePipe', () => { // Then - expect(pipe).toBeTruthy(); - }); + expect(result).toBe(""); + }); + + it("should verify on missing schedule returns default value", () => { + // Given + const schedule: string = null; + const defaultResult = "default001"; + + //When + const result = pipe.transform(schedule, defaultResult); + + // Then + expect(result).toBe(defaultResult); + }); + + it('should verify will correctly translate cron "5 5 5 2 *"', () => { + // Given + const schedule = "5 5 5 2 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain("At 05:05 AM"); + }); + + it('should verify will return parsing error message invalid cron "65 65 65 65 65"', () => { + // Given + const schedule = "65 65 65 65 65"; - describe('Methods::', () => { - describe('|transform|', () => { - it('should verify on missing schedule returns empty result', () => { - // Given - const schedule: string = null; + // When + const result = pipe.transform(schedule); - // When - const result = pipe.transform(schedule); + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + + describe("testing minute range", () => { + it('should verify will correctly translate cron "0 11 10 7 *"', () => { + // Given + const schedule = "0 11 10 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:00 AM, on day 10 of the month, only in July", + ); + }); + + it('should verify will correctly translate cron "59 11 10 7 *"', () => { + // Given + const schedule = "59 11 10 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:59 AM, on day 10 of the month, only in July", + ); + }); + + it('should verify will return parsing error message invalid cron "60 11 10 7 *"', () => { + // Given + const schedule = "60 12 10 7 *"; - // Then - expect(result).toBe(''); - }); + // When + const result = pipe.transform(schedule); - it('should verify on missing schedule returns default value', () => { - // Given - const schedule: string = null; - const defaultResult = 'default001'; + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + }); + + describe("testing hour range", () => { + it('should verify will correctly translate cron "30 0 10 7 *"', () => { + // Given + const schedule = "30 0 10 7 *"; - //When - const result = pipe.transform(schedule, defaultResult); + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 12:30 AM, on day 10 of the month, only in July", + ); + }); - // Then - expect(result).toBe(defaultResult); - }); + it('should verify will correctly translate cron "30 23 10 7 *"', () => { + // Given + const schedule = "30 23 10 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 PM, on day 10 of the month, only in July", + ); + }); - it('should verify will correctly translate cron "5 5 5 2 *"', () => { - // Given - const schedule = '5 5 5 2 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 05:05 AM'); - }); - - it('should verify will return parsing error message invalid cron "65 65 65 65 65"', () => { - // Given - const schedule = '65 65 65 65 65'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - - describe('testing minute range', () => { - it('should verify will correctly translate cron "0 11 10 7 *"', () => { - // Given - const schedule = '0 11 10 7 *'; + it('should verify will return parsing error message invalid cron "30 24 10 7 *"', () => { + // Given + const schedule = "30 24 10 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + }); + + describe("testing day range", () => { + it('should verify will correctly translate cron "30 11 1 7 *"', () => { + // Given + const schedule = "30 11 1 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 1 of the month, only in July", + ); + }); + + it('should verify will correctly translate cron "30 11 31 7 *"', () => { + // Given + const schedule = "30 11 31 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 31 of the month, only in July", + ); + }); + + it('should verify will return parsing error message invalid cron "30 11 0 7 *"', () => { + // Given + const schedule = "30 11 0 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + + it('should verify will return parsing error message invalid cron "30 11 32 7 *"', () => { + // Given + const schedule = "30 11 32 7 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + }); + + describe("testing month range", () => { + it('should verify will correctly translate cron "30 11 10 1 *"', () => { + // Given + const schedule = "30 11 10 1 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 10 of the month, only in January", + ); + }); + + it('should verify will correctly translate cron "30 11 10 12 *"', () => { + // Given + const schedule = "30 11 10 12 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 10 of the month, only in December", + ); + }); - // When - const result = pipe.transform(schedule); + it('should verify will return parsing error message invalid cron "30 11 10 0 *"', () => { + // Given + const schedule = "30 11 10 0 *"; - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:00 AM, on day 10 of the month, only in July'); - }); + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + + it('should verify will return parsing error message invalid cron "30 11 10 13 *"', () => { + // Given + const schedule = "30 11 10 13 *"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + }); + + describe("testing day of week range", () => { + it('should verify will correctly translate cron "30 11 10 7 0"', () => { + // Given + const schedule = "30 11 10 7 0"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 10 of the month, and on Sunday, only in July", + ); + }); + + it('should verify will correctly translate cron "30 11 10 7 6"', () => { + // Given + const schedule = "30 11 10 7 6"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 10 of the month, and on Saturday, only in July", + ); + }); + + it('should verify will correctly translate cron "30 11 10 7 7"', () => { + // Given + const schedule = "30 11 10 7 7"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "At 11:30 AM, on day 10 of the month, and on Sunday, only in July", + ); + }); + + it('should verify will return parsing error message invalid cron "30 11 10 7 8"', () => { + // Given + const schedule = "30 11 10 7 8"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + }); + + describe("testing non-standard cron expressions", () => { + it('should verify will correctly translate cron "@hourly"', () => { + // Given + const schedule = "@hourly"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "Run once an hour at the beginning of the hour", + ); + }); + + it('should verify will correctly translate cron "@daily" and "@midnight"', () => { + // Given + const schedule1 = "@daily"; + const schedule2 = "@midnight"; + + // When + const result1 = pipe.transform(schedule1); + const result2 = pipe.transform(schedule2); + + // Then + expect(result1).toBeDefined(); + expect(result1).toContain("Run once a day at midnight"); + expect(result2).toBeDefined(); + expect(result2).toContain("Run once a day at midnight"); + }); + + it('should verify will correctly translate cron "@weekly"', () => { + // Given + const schedule = "@weekly"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "Run once a week at midnight on Sunday morning", + ); + }); + + it('should verify will correctly translate cron "@monthly"', () => { + // Given + const schedule = "@monthly"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain( + "Run once a month at midnight of the first day of the month", + ); + }); + + it('should verify will correctly translate cron "@yearly" and "@annually"', () => { + // Given + const schedule1 = "@yearly"; + const schedule2 = "@annually"; + + // When + const result1 = pipe.transform(schedule1); + const result2 = pipe.transform(schedule2); + + // Then + expect(result1).toBeDefined(); + expect(result1).toContain("Run once a year at midnight of 1 January"); + expect(result2).toBeDefined(); + expect(result2).toContain("Run once a year at midnight of 1 January"); + }); + + it('should verify will return parsing error message invalid cron "yearly"', () => { + // Given + const schedule = "yearly"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + + it('should verify will return parsing error message invalid cron "some random text"', () => { + // Given + const schedule = "some random text"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); + + it('should verify will return parsing error message invalid cron "some random text"', () => { + // Given + const schedule = "some random text"; + + // When + const result = pipe.transform(schedule); + + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); + }); - it('should verify will correctly translate cron "59 11 10 7 *"', () => { - // Given - const schedule = '59 11 10 7 *'; + it('should verify will return parsing error message invalid cron "yearly"', () => { + // Given + const schedule = "yearly"; - // When - const result = pipe.transform(schedule); + // When + const result = pipe.transform(schedule); - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:59 AM, on day 10 of the month, only in July'); - }); - - it('should verify will return parsing error message invalid cron "60 11 10 7 *"', () => { - // Given - const schedule = '60 12 10 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - }); - - describe('testing hour range', () => { - it('should verify will correctly translate cron "30 0 10 7 *"', () => { - // Given - const schedule = '30 0 10 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 12:30 AM, on day 10 of the month, only in July'); - }); - - it('should verify will correctly translate cron "30 23 10 7 *"', () => { - // Given - const schedule = '30 23 10 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 PM, on day 10 of the month, only in July'); - }); - - it('should verify will return parsing error message invalid cron "30 24 10 7 *"', () => { - // Given - const schedule = '30 24 10 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - }); - - describe('testing day range', () => { - it('should verify will correctly translate cron "30 11 1 7 *"', () => { - // Given - const schedule = '30 11 1 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 1 of the month, only in July'); - }); - - it('should verify will correctly translate cron "30 11 31 7 *"', () => { - // Given - const schedule = '30 11 31 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 31 of the month, only in July'); - }); - - it('should verify will return parsing error message invalid cron "30 11 0 7 *"', () => { - // Given - const schedule = '30 11 0 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - - it('should verify will return parsing error message invalid cron "30 11 32 7 *"', () => { - // Given - const schedule = '30 11 32 7 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - }); - - describe('testing month range', () => { - it('should verify will correctly translate cron "30 11 10 1 *"', () => { - // Given - const schedule = '30 11 10 1 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 10 of the month, only in January'); - }); - - it('should verify will correctly translate cron "30 11 10 12 *"', () => { - // Given - const schedule = '30 11 10 12 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 10 of the month, only in December'); - }); - - it('should verify will return parsing error message invalid cron "30 11 10 0 *"', () => { - // Given - const schedule = '30 11 10 0 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - - it('should verify will return parsing error message invalid cron "30 11 10 13 *"', () => { - // Given - const schedule = '30 11 10 13 *'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - }); - - describe('testing day of week range', () => { - it('should verify will correctly translate cron "30 11 10 7 0"', () => { - // Given - const schedule = '30 11 10 7 0'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 10 of the month, and on Sunday, only in July'); - }); - - it('should verify will correctly translate cron "30 11 10 7 6"', () => { - // Given - const schedule = '30 11 10 7 6'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 10 of the month, and on Saturday, only in July'); - }); - - it('should verify will correctly translate cron "30 11 10 7 7"', () => { - // Given - const schedule = '30 11 10 7 7'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('At 11:30 AM, on day 10 of the month, and on Sunday, only in July'); - }); - - it('should verify will return parsing error message invalid cron "30 11 10 7 8"', () => { - // Given - const schedule = '30 11 10 7 8'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - }); - - describe('testing non-standard cron expressions', () => { - it('should verify will correctly translate cron "@hourly"', () => { - // Given - const schedule = '@hourly'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('Run once an hour at the beginning of the hour'); - }); - - it('should verify will correctly translate cron "@daily" and "@midnight"', () => { - // Given - const schedule1 = '@daily'; - const schedule2 = '@midnight'; - - // When - const result1 = pipe.transform(schedule1); - const result2 = pipe.transform(schedule2); - - // Then - expect(result1).toBeDefined(); - expect(result1).toContain('Run once a day at midnight'); - expect(result2).toBeDefined(); - expect(result2).toContain('Run once a day at midnight'); - }); - - it('should verify will correctly translate cron "@weekly"', () => { - // Given - const schedule = '@weekly'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('Run once a week at midnight on Sunday morning'); - }); - - it('should verify will correctly translate cron "@monthly"', () => { - // Given - const schedule = '@monthly'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain('Run once a month at midnight of the first day of the month'); - }); - - it('should verify will correctly translate cron "@yearly" and "@annually"', () => { - // Given - const schedule1 = '@yearly'; - const schedule2 = '@annually'; - - // When - const result1 = pipe.transform(schedule1); - const result2 = pipe.transform(schedule2); - - // Then - expect(result1).toBeDefined(); - expect(result1).toContain('Run once a year at midnight of 1 January'); - expect(result2).toBeDefined(); - expect(result2).toContain('Run once a year at midnight of 1 January'); - }); - - it('should verify will return parsing error message invalid cron "yearly"', () => { - // Given - const schedule = 'yearly'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - - it('should verify will return parsing error message invalid cron "some random text"', () => { - // Given - const schedule = 'some random text'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - - it('should verify will return parsing error message invalid cron "some random text"', () => { - // Given - const schedule = 'some random text'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - - it('should verify will return parsing error message invalid cron "yearly"', () => { - // Given - const schedule = 'yearly'; - - // When - const result = pipe.transform(schedule); - - // Then - expect(result).toBeDefined(); - expect(result).toContain(`Invalid Cron expression "${schedule}"`); - }); - }); + // Then + expect(result).toBeDefined(); + expect(result).toContain(`Invalid Cron expression "${schedule}"`); }); + }); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.ts index 6c3dd08b38..91071653e6 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/format-schedule.pipe.ts @@ -5,69 +5,75 @@ /* eslint-disable no-underscore-dangle */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; -import cronstrue from 'cronstrue'; +import cronstrue from "cronstrue"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; @Pipe({ - name: 'formatSchedule' + name: "formatSchedule", }) export class FormatSchedulePipe implements PipeTransform { - private static _fallbackTransformNonStandardCron(cron: string): string { - const match = `${cron}`.trim().match(/^@hourly|@daily|@midnight|@weekly|@monthly|@yearly|@annually$/); + private static _fallbackTransformNonStandardCron(cron: string): string { + const match = `${cron}` + .trim() + .match(/^@hourly|@daily|@midnight|@weekly|@monthly|@yearly|@annually$/); - if (CollectionsUtil.isNil(match)) { - throw new Error('Cron expression cannot be null or undefined.'); - } + if (CollectionsUtil.isNil(match)) { + throw new Error("Cron expression cannot be null or undefined."); + } - switch (match.input) { - case '@hourly': - return 'Run once an hour at the beginning of the hour'; - case '@daily': - case '@midnight': - return 'Run once a day at midnight'; - case '@weekly': - return 'Run once a week at midnight on Sunday morning'; - case '@monthly': - return 'Run once a month at midnight of the first day of the month'; - case '@yearly': - case '@annually': - return 'Run once a year at midnight of 1 January'; - default: - throw new Error('Cron expression is NOT nonstandard predefined scheduling definition.'); - } + switch (match.input) { + case "@hourly": + return "Run once an hour at the beginning of the hour"; + case "@daily": + case "@midnight": + return "Run once a day at midnight"; + case "@weekly": + return "Run once a week at midnight on Sunday morning"; + case "@monthly": + return "Run once a month at midnight of the first day of the month"; + case "@yearly": + case "@annually": + return "Run once a year at midnight of 1 January"; + default: + throw new Error( + "Cron expression is NOT nonstandard predefined scheduling definition.", + ); } + } - /** - * @inheritDoc - * - * - Cron schedule default format from kubernetes https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ - * - Time in UTC - */ - transform(cronSchedule: string, defaultResult?: string): string { - try { - const defaultValue = defaultResult ?? ''; + /** + * @inheritDoc + * + * - Cron schedule default format from kubernetes https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/ + * - Time in UTC + */ + transform(cronSchedule: string, defaultResult?: string): string { + try { + const defaultValue = defaultResult ?? ""; - if (!cronSchedule) { - return defaultValue; - } + if (!cronSchedule) { + return defaultValue; + } - //TODO : https://github.com/bradymholt/cRonstrue/issues/94 - // cronstrue doesn't support timezones. Need to use another library - return cronstrue.toString(cronSchedule, { - monthStartIndexZero: false, - dayOfWeekStartIndexZero: true - }); - } catch (e) { - try { - return FormatSchedulePipe._fallbackTransformNonStandardCron(cronSchedule); - } catch (_e) { - console.error(`Parsing error. Cron expression "${cronSchedule}"`); + //TODO : https://github.com/bradymholt/cRonstrue/issues/94 + // cronstrue doesn't support timezones. Need to use another library + return cronstrue.toString(cronSchedule, { + monthStartIndexZero: false, + dayOfWeekStartIndexZero: true, + }); + } catch (e) { + try { + return FormatSchedulePipe._fallbackTransformNonStandardCron( + cronSchedule, + ); + } catch (_e) { + console.error(`Parsing error. Cron expression "${cronSchedule}"`); - return `Invalid Cron expression "${cronSchedule}"`; - } - } + return `Invalid Cron expression "${cronSchedule}"`; + } } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/index.ts index 4a7c10f8c5..f41d49daf2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/index.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './contacts-present.pipe'; -export * from './execution-success-rate.pipe'; -export * from './extract-job-status.pipe'; -export * from './format-delta.pipe'; -export * from './format-schedule.pipe'; -export * from './parse-epoch.pipe'; -export * from './parse-next-run.pipe'; -export * from './extract-contacts.pipe'; +export * from "./contacts-present.pipe"; +export * from "./execution-success-rate.pipe"; +export * from "./extract-job-status.pipe"; +export * from "./format-delta.pipe"; +export * from "./format-schedule.pipe"; +export * from "./parse-epoch.pipe"; +export * from "./parse-next-run.pipe"; +export * from "./extract-contacts.pipe"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.spec.ts index 50c9d3ecef..8f487ec1e1 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.spec.ts @@ -3,30 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { ParseEpochPipe } from './parse-epoch.pipe'; +import { TestBed } from "@angular/core/testing"; +import { ParseEpochPipe } from "./parse-epoch.pipe"; -describe('ParseEpochPipe', () => { - let pipe: ParseEpochPipe; - const TEST_EPOCH_SECONDS = 1522668899; - const MILLIS_MULTIPLIER = 1000; +describe("ParseEpochPipe", () => { + let pipe: ParseEpochPipe; + const TEST_EPOCH_SECONDS = 1522668899; + const MILLIS_MULTIPLIER = 1000; - beforeEach(() => { - TestBed.configureTestingModule({ providers: [ParseEpochPipe] }); - pipe = TestBed.inject(ParseEpochPipe); - }); + beforeEach(() => { + TestBed.configureTestingModule({ providers: [ParseEpochPipe] }); + pipe = TestBed.inject(ParseEpochPipe); + }); - it('can instantiate', () => { - expect(pipe).toBeTruthy(); - }); + it("can instantiate", () => { + expect(pipe).toBeTruthy(); + }); - it('transforms missing epoch to emtpy result', () => { - expect(pipe.transform(-1)).toBeNull(); - }); + it("transforms missing epoch to emtpy result", () => { + expect(pipe.transform(-1)).toBeNull(); + }); - it('transforms valid epoch to valid result', () => { - const result = pipe.transform(TEST_EPOCH_SECONDS); - expect(result).toBeDefined(); - expect(result).toEqual(new Date(TEST_EPOCH_SECONDS * MILLIS_MULTIPLIER)); - }); + it("transforms valid epoch to valid result", () => { + const result = pipe.transform(TEST_EPOCH_SECONDS); + expect(result).toBeDefined(); + expect(result).toEqual(new Date(TEST_EPOCH_SECONDS * MILLIS_MULTIPLIER)); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.ts index bc7c45199c..678a9d9c5d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-epoch.pipe.ts @@ -3,33 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; @Pipe({ - name: 'parseEpoch' + name: "parseEpoch", }) export class ParseEpochPipe implements PipeTransform { - /** - * ** Transform to Epoch time. - * - * - This method should be equal to instance method. - * - Methods: {@link ParseEpochPipe.transform} - */ - static transform(nextRunEpochSeconds: number): Date { - if (nextRunEpochSeconds < 0) { - return null; - } - - return new Date(nextRunEpochSeconds * 1000); + /** + * ** Transform to Epoch time. + * + * - This method should be equal to instance method. + * - Methods: {@link ParseEpochPipe.transform} + */ + static transform(nextRunEpochSeconds: number): Date { + if (nextRunEpochSeconds < 0) { + return null; } - /** - * @inheritDoc - * - * - This method should be equal to instance method. - * - Methods: {@link ParseEpochPipe.transform} - */ - transform(nextRunEpochSeconds: number): Date { - return ParseEpochPipe.transform(nextRunEpochSeconds); - } + return new Date(nextRunEpochSeconds * 1000); + } + + /** + * @inheritDoc + * + * - This method should be equal to instance method. + * - Methods: {@link ParseEpochPipe.transform} + */ + transform(nextRunEpochSeconds: number): Date { + return ParseEpochPipe.transform(nextRunEpochSeconds); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.spec.ts index 40a98fcc93..706454be2d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.spec.ts @@ -3,169 +3,191 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { ParseNextRunPipe } from './parse-next-run.pipe'; - -describe('ParseNextRunPipe', () => { - let pipe: ParseNextRunPipe; - - beforeEach(() => { - TestBed.configureTestingModule({ providers: [ParseNextRunPipe] }); - pipe = TestBed.inject(ParseNextRunPipe); - }); - - it('can instantiate ParseNextRunPipe', () => { - expect(pipe).toBeTruthy(); - }); - - describe('Methods::', () => { - describe('|transform|', () => { - it('transforms invalid cron to null date', () => { - const cron: string = null; - const nextExecution = 1; - expect(pipe.transform(cron, nextExecution)).toBe(null); - }); - - it('transforms invalid nextExecution to valid date', () => { - const cron = '* * * * *'; - const nextExecution: number = null; - expect(pipe.transform(cron, nextExecution)).toBeDefined(); - }); - - it('transforms missing nextExecution to valid date', () => { - const cron = '* * * * *'; - expect(pipe.transform(cron)).toBeDefined(); - }); - - it('transforms valid cron to valid date', () => { - const cron = '5 5 5 2 *'; - const result = pipe.transform(cron); - expect(result).toBeDefined(); - expect(result.getMonth()).toBe(1); // 1 means Feb - }); - - it(`should verify will return next first run for cron "0 12 * * *" to DateTime in UTC`, (done) => { - // Given - const cron = '0 12 * * *'; - const expected = new Date(); - let useTimeout = false; - - if (expected.getUTCHours() === 11 && expected.getUTCMinutes() === 59 && expected.getUTCSeconds() === 59) { - useTimeout = true; - expected.setUTCDate(expected.getUTCDate() + 1); - } else if (expected.getUTCHours() >= 12) { - expected.setUTCDate(expected.getUTCDate() + 1); - } - - const y = expected.getUTCFullYear(); - const _m: number = expected.getUTCMonth() + 1; - const _d: number = expected.getUTCDate(); - - let m = `${_m}`; - let d = `${_d}`; - - if (_m < 10) { - m = `0${_m}`; - } - - if (_d < 10) { - d = `0${_d}`; - } - - if (useTimeout) { - setTimeout(() => { - // When - const date = pipe.transform(cron, 1); - - // Then - console.log(date.toISOString()); - expect(date.toISOString()).toEqual(`${y}-${m}-${d}T12:00:00.000Z`); - - done(); - }); - } else { - // When - const date = pipe.transform(cron, 1); - - // Then - console.log(date.toISOString()); - expect(date.toISOString()).toEqual(`${y}-${m}-${d}T12:00:00.000Z`); - - done(); - } - }, 5000); - - it(`should verify will return next second run for cron "45 0/12 * * *" to DateTime in UTC`, (done) => { - // Given - const cron = '45 0/12 * * *'; - const expected = new Date(); - let useTimeout = false; - let lunchTime = false; - - let hh: string; - let mm: string; - - if (expected.getUTCHours() === 12 && expected.getUTCMinutes() === 44 && expected.getUTCSeconds() === 59) { - lunchTime = true; - useTimeout = true; - expected.setUTCDate(expected.getUTCDate() + 1); - } else if (expected.getUTCHours() === 0 && expected.getUTCMinutes() === 44 && expected.getUTCSeconds() === 59) { - useTimeout = true; - expected.setUTCDate(expected.getUTCDate() + 1); - } else if ((expected.getUTCHours() >= 12 && expected.getUTCMinutes() >= 45) || expected.getUTCHours() >= 13) { - lunchTime = true; - expected.setUTCDate(expected.getUTCDate() + 1); - } else if (expected.getUTCHours() === 0 && expected.getUTCMinutes() < 45) { - lunchTime = true; - } else { - expected.setUTCDate(expected.getUTCDate() + 1); - } - - const y = expected.getUTCFullYear(); - const _m: number = expected.getUTCMonth() + 1; - const _d: number = expected.getUTCDate(); - - let m = `${_m}`; - let d = `${_d}`; - - if (_m < 10) { - m = `0${m}`; - } - - if (_d < 10) { - d = `0${d}`; - } - - if (lunchTime) { - hh = '12'; - mm = '45'; - } else { - hh = '00'; - mm = '45'; - } - - if (useTimeout) { - setTimeout(() => { - // When - const date = pipe.transform(cron, 2); - - // Then - console.log(date.toISOString()); - expect(date.toISOString()).toEqual(`${y}-${m}-${d}T${hh}:${mm}:00.000Z`); - - done(); - }, 2000); - } else { - // When - const date = pipe.transform(cron, 2); - - // Then - console.log(date.toISOString()); - expect(date.toISOString()).toEqual(`${y}-${m}-${d}T${hh}:${mm}:00.000Z`); - - done(); - } - }, 10000); - }); +import { TestBed } from "@angular/core/testing"; +import { ParseNextRunPipe } from "./parse-next-run.pipe"; + +describe("ParseNextRunPipe", () => { + let pipe: ParseNextRunPipe; + + beforeEach(() => { + TestBed.configureTestingModule({ providers: [ParseNextRunPipe] }); + pipe = TestBed.inject(ParseNextRunPipe); + }); + + it("can instantiate ParseNextRunPipe", () => { + expect(pipe).toBeTruthy(); + }); + + describe("Methods::", () => { + describe("|transform|", () => { + it("transforms invalid cron to null date", () => { + const cron: string = null; + const nextExecution = 1; + expect(pipe.transform(cron, nextExecution)).toBe(null); + }); + + it("transforms invalid nextExecution to valid date", () => { + const cron = "* * * * *"; + const nextExecution: number = null; + expect(pipe.transform(cron, nextExecution)).toBeDefined(); + }); + + it("transforms missing nextExecution to valid date", () => { + const cron = "* * * * *"; + expect(pipe.transform(cron)).toBeDefined(); + }); + + it("transforms valid cron to valid date", () => { + const cron = "5 5 5 2 *"; + const result = pipe.transform(cron); + expect(result).toBeDefined(); + expect(result.getMonth()).toBe(1); // 1 means Feb + }); + + it(`should verify will return next first run for cron "0 12 * * *" to DateTime in UTC`, (done) => { + // Given + const cron = "0 12 * * *"; + const expected = new Date(); + let useTimeout = false; + + if ( + expected.getUTCHours() === 11 && + expected.getUTCMinutes() === 59 && + expected.getUTCSeconds() === 59 + ) { + useTimeout = true; + expected.setUTCDate(expected.getUTCDate() + 1); + } else if (expected.getUTCHours() >= 12) { + expected.setUTCDate(expected.getUTCDate() + 1); + } + + const y = expected.getUTCFullYear(); + const _m: number = expected.getUTCMonth() + 1; + const _d: number = expected.getUTCDate(); + + let m = `${_m}`; + let d = `${_d}`; + + if (_m < 10) { + m = `0${_m}`; + } + + if (_d < 10) { + d = `0${_d}`; + } + + if (useTimeout) { + setTimeout(() => { + // When + const date = pipe.transform(cron, 1); + + // Then + console.log(date.toISOString()); + expect(date.toISOString()).toEqual(`${y}-${m}-${d}T12:00:00.000Z`); + + done(); + }); + } else { + // When + const date = pipe.transform(cron, 1); + + // Then + console.log(date.toISOString()); + expect(date.toISOString()).toEqual(`${y}-${m}-${d}T12:00:00.000Z`); + + done(); + } + }, 5000); + + it(`should verify will return next second run for cron "45 0/12 * * *" to DateTime in UTC`, (done) => { + // Given + const cron = "45 0/12 * * *"; + const expected = new Date(); + let useTimeout = false; + let lunchTime = false; + + let hh: string; + let mm: string; + + if ( + expected.getUTCHours() === 12 && + expected.getUTCMinutes() === 44 && + expected.getUTCSeconds() === 59 + ) { + lunchTime = true; + useTimeout = true; + expected.setUTCDate(expected.getUTCDate() + 1); + } else if ( + expected.getUTCHours() === 0 && + expected.getUTCMinutes() === 44 && + expected.getUTCSeconds() === 59 + ) { + useTimeout = true; + expected.setUTCDate(expected.getUTCDate() + 1); + } else if ( + (expected.getUTCHours() >= 12 && expected.getUTCMinutes() >= 45) || + expected.getUTCHours() >= 13 + ) { + lunchTime = true; + expected.setUTCDate(expected.getUTCDate() + 1); + } else if ( + expected.getUTCHours() === 0 && + expected.getUTCMinutes() < 45 + ) { + lunchTime = true; + } else { + expected.setUTCDate(expected.getUTCDate() + 1); + } + + const y = expected.getUTCFullYear(); + const _m: number = expected.getUTCMonth() + 1; + const _d: number = expected.getUTCDate(); + + let m = `${_m}`; + let d = `${_d}`; + + if (_m < 10) { + m = `0${m}`; + } + + if (_d < 10) { + d = `0${d}`; + } + + if (lunchTime) { + hh = "12"; + mm = "45"; + } else { + hh = "00"; + mm = "45"; + } + + if (useTimeout) { + setTimeout(() => { + // When + const date = pipe.transform(cron, 2); + + // Then + console.log(date.toISOString()); + expect(date.toISOString()).toEqual( + `${y}-${m}-${d}T${hh}:${mm}:00.000Z`, + ); + + done(); + }, 2000); + } else { + // When + const date = pipe.transform(cron, 2); + + // Then + console.log(date.toISOString()); + expect(date.toISOString()).toEqual( + `${y}-${m}-${d}T${hh}:${mm}:00.000Z`, + ); + + done(); + } + }, 10000); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.ts index f17aa55ed1..c029c5b0e9 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/pipes/parse-next-run.pipe.ts @@ -3,36 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; -import * as parser from 'cron-parser'; +import * as parser from "cron-parser"; @Pipe({ - name: 'parseNextRun' + name: "parseNextRun", }) export class ParseNextRunPipe implements PipeTransform { - /** - * @inheritDoc - */ - transform(cron: string, nextExecution?: number): Date { - if (!cron) { - return null; - } + /** + * @inheritDoc + */ + transform(cron: string, nextExecution?: number): Date { + if (!cron) { + return null; + } - if (!nextExecution) { - nextExecution = 1; - } + if (!nextExecution) { + nextExecution = 1; + } - let result: Date; - try { - const parsedDate = parser.parseExpression(cron, { utc: true }); - for (let i = 0; i < nextExecution; i++) { - result = parsedDate.next().toDate(); - } - } catch (e) { - result = null; - console.error('Error parsing next run', e); - } - return result; + let result: Date; + try { + const parsedDate = parser.parseExpression(cron, { utc: true }); + for (let i = 0; i < nextExecution; i++) { + result = parsedDate.next().toDate(); + } + } catch (e) { + result = null; + console.error("Error parsing next run", e); } + return result; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.spec.ts index d8269057bc..e64c6b1566 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.spec.ts @@ -3,18 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CronUtil } from './cron.util'; +import { CronUtil } from "./cron.util"; -describe('DateUtil', () => { - it('expect null argument to return null result', () => { - expect(CronUtil.getNextExecutionErrors(null)).toBe('No schedule cron configured for this job'); - }); +describe("DateUtil", () => { + it("expect null argument to return null result", () => { + expect(CronUtil.getNextExecutionErrors(null)).toBe( + "No schedule cron configured for this job", + ); + }); - it('expect invalid argument to return error text', () => { - expect(CronUtil.getNextExecutionErrors('* * * * * * *')).toContain(''); - }); + it("expect invalid argument to return error text", () => { + expect(CronUtil.getNextExecutionErrors("* * * * * * *")).toContain(""); + }); - it('expect valid argument to return null', () => { - expect(CronUtil.getNextExecutionErrors('5 5 5 2 *')).toEqual(null); - }); + it("expect valid argument to return null", () => { + expect(CronUtil.getNextExecutionErrors("5 5 5 2 *")).toEqual(null); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.ts index b7ec14d47f..2b577d1055 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/cron.util.ts @@ -3,19 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as parser from 'cron-parser'; +import * as parser from "cron-parser"; export class CronUtil { - static getNextExecutionErrors(cron: string): string { - if (!cron) { - return 'No schedule cron configured for this job'; - } - try { - parser.parseExpression(cron); - return null; // parsing successful, reset flag - } catch (e: unknown) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `Could not extract next executions from the cron expression: ${e}`; - } + static getNextExecutionErrors(cron: string): string { + if (!cron) { + return "No schedule cron configured for this job"; } + try { + parser.parseExpression(cron); + return null; // parsing successful, reset flag + } catch (e: unknown) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `Could not extract next executions from the cron expression: ${e}`; + } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.spec.ts index 850854ff91..bbb0dc95d1 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.spec.ts @@ -6,251 +6,267 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { - DataJobExecution, - DataJobExecutionDetails, - DataJobExecutionStatus, - DataJobExecutionStatusDeprecated, - DataJobExecutionType -} from '../../model'; - -import { DataJobUtil } from './data-job.util'; - -describe('DataJobUtil', () => { - describe('|isJobRunningPredicate|', () => { - it('should verify will return true if status is RUNNING', () => { - // Given - const execution: DataJobExecutionDetails = { - status: DataJobExecutionStatusDeprecated.RUNNING - } as any; - - // When - const response = DataJobUtil.isJobRunningPredicate(execution); - - // Then - expect(response).toBeTrue(); - }); - - it('should verify will return true if status is SUBMITTED', () => { - // Given - const execution: DataJobExecutionDetails = { - status: DataJobExecutionStatusDeprecated.SUBMITTED - } as any; - - // When - const response = DataJobUtil.isJobRunningPredicate(execution); - - // Then - expect(response).toBeTrue(); - }); - - it('should verify will return false if status is different from RUNNING or SUBMITTED', () => { - // Given - const execution1: DataJobExecutionDetails = { - status: DataJobExecutionStatusDeprecated.USER_ERROR - } as any; - const execution2: DataJobExecutionDetails = { - status: DataJobExecutionStatusDeprecated.SKIPPED - } as any; - const execution3: DataJobExecutionDetails = { - status: DataJobExecutionStatusDeprecated.PLATFORM_ERROR - } as any; - const execution4: DataJobExecutionDetails = { - status: DataJobExecutionStatusDeprecated.SUCCEEDED - } as any; - - // When - const response1 = DataJobUtil.isJobRunningPredicate(execution1); - const response2 = DataJobUtil.isJobRunningPredicate(execution2); - const response3 = DataJobUtil.isJobRunningPredicate(execution3); - const response4 = DataJobUtil.isJobRunningPredicate(execution4); - - // Then - expect(response1).toBeFalse(); - expect(response2).toBeFalse(); - expect(response3).toBeFalse(); - expect(response4).toBeFalse(); - }); + DataJobExecution, + DataJobExecutionDetails, + DataJobExecutionStatus, + DataJobExecutionStatusDeprecated, + DataJobExecutionType, +} from "../../model"; + +import { DataJobUtil } from "./data-job.util"; + +describe("DataJobUtil", () => { + describe("|isJobRunningPredicate|", () => { + it("should verify will return true if status is RUNNING", () => { + // Given + const execution: DataJobExecutionDetails = { + status: DataJobExecutionStatusDeprecated.RUNNING, + } as any; + + // When + const response = DataJobUtil.isJobRunningPredicate(execution); + + // Then + expect(response).toBeTrue(); }); - describe('|isJobRunning|', () => { - it('should verify will invoke correct method', () => { - // Given - const spy = spyOn(DataJobUtil, 'isJobRunningPredicate').and.callThrough(); - const executions: DataJobExecutionDetails[] = [{ status: DataJobExecutionStatusDeprecated.RUNNING }] as any; - - // When - DataJobUtil.isJobRunning(executions); - - // Then - expect(spy.calls.argsFor(0)[0]).toEqual(executions[0]); - }); - - it('should verify will invoke correct method until find RUNNING or SUBMITTED status', () => { - // Given - const spy = spyOn(DataJobUtil, 'isJobRunningPredicate').and.callThrough(); - const executions: DataJobExecutionDetails[] = [ - { status: DataJobExecutionStatusDeprecated.FAILED }, - { status: DataJobExecutionStatusDeprecated.PLATFORM_ERROR }, - { status: DataJobExecutionStatusDeprecated.SUBMITTED }, - { status: DataJobExecutionStatusDeprecated.RUNNING } - ] as any; - - // When - DataJobUtil.isJobRunning(executions); - - // Then - expect(spy).toHaveBeenCalledTimes(3); - expect(spy.calls.argsFor(0)[0]).toEqual(executions[0]); - expect(spy.calls.argsFor(1)[0]).toEqual(executions[1]); - expect(spy.calls.argsFor(2)[0]).toEqual(executions[2]); - }); - - it('should verify will invoke correct method with all elements no RUNNING or SUBMITTED status', () => { - // Given - const spy = spyOn(DataJobUtil, 'isJobRunningPredicate').and.callThrough(); - const executions: DataJobExecutionDetails[] = [ - { status: DataJobExecutionStatusDeprecated.FAILED }, - { status: DataJobExecutionStatusDeprecated.PLATFORM_ERROR }, - { status: DataJobExecutionStatusDeprecated.SUCCEEDED }, - { status: DataJobExecutionStatusDeprecated.SKIPPED } - ] as any; - - // When - DataJobUtil.isJobRunning(executions); - - // Then - expect(spy).toHaveBeenCalledTimes(4); - expect(spy.calls.argsFor(0)[0]).toEqual(executions[0]); - expect(spy.calls.argsFor(1)[0]).toEqual(executions[1]); - expect(spy.calls.argsFor(2)[0]).toEqual(executions[2]); - expect(spy.calls.argsFor(3)[0]).toEqual(executions[3]); - }); + it("should verify will return true if status is SUBMITTED", () => { + // Given + const execution: DataJobExecutionDetails = { + status: DataJobExecutionStatusDeprecated.SUBMITTED, + } as any; + + // When + const response = DataJobUtil.isJobRunningPredicate(execution); + + // Then + expect(response).toBeTrue(); + }); + + it("should verify will return false if status is different from RUNNING or SUBMITTED", () => { + // Given + const execution1: DataJobExecutionDetails = { + status: DataJobExecutionStatusDeprecated.USER_ERROR, + } as any; + const execution2: DataJobExecutionDetails = { + status: DataJobExecutionStatusDeprecated.SKIPPED, + } as any; + const execution3: DataJobExecutionDetails = { + status: DataJobExecutionStatusDeprecated.PLATFORM_ERROR, + } as any; + const execution4: DataJobExecutionDetails = { + status: DataJobExecutionStatusDeprecated.SUCCEEDED, + } as any; + + // When + const response1 = DataJobUtil.isJobRunningPredicate(execution1); + const response2 = DataJobUtil.isJobRunningPredicate(execution2); + const response3 = DataJobUtil.isJobRunningPredicate(execution3); + const response4 = DataJobUtil.isJobRunningPredicate(execution4); + + // Then + expect(response1).toBeFalse(); + expect(response2).toBeFalse(); + expect(response3).toBeFalse(); + expect(response4).toBeFalse(); + }); + }); + + describe("|isJobRunning|", () => { + it("should verify will invoke correct method", () => { + // Given + const spy = spyOn(DataJobUtil, "isJobRunningPredicate").and.callThrough(); + const executions: DataJobExecutionDetails[] = [ + { status: DataJobExecutionStatusDeprecated.RUNNING }, + ] as any; + + // When + DataJobUtil.isJobRunning(executions); + + // Then + expect(spy.calls.argsFor(0)[0]).toEqual(executions[0]); + }); + + it("should verify will invoke correct method until find RUNNING or SUBMITTED status", () => { + // Given + const spy = spyOn(DataJobUtil, "isJobRunningPredicate").and.callThrough(); + const executions: DataJobExecutionDetails[] = [ + { status: DataJobExecutionStatusDeprecated.FAILED }, + { status: DataJobExecutionStatusDeprecated.PLATFORM_ERROR }, + { status: DataJobExecutionStatusDeprecated.SUBMITTED }, + { status: DataJobExecutionStatusDeprecated.RUNNING }, + ] as any; + + // When + DataJobUtil.isJobRunning(executions); + + // Then + expect(spy).toHaveBeenCalledTimes(3); + expect(spy.calls.argsFor(0)[0]).toEqual(executions[0]); + expect(spy.calls.argsFor(1)[0]).toEqual(executions[1]); + expect(spy.calls.argsFor(2)[0]).toEqual(executions[2]); + }); + + it("should verify will invoke correct method with all elements no RUNNING or SUBMITTED status", () => { + // Given + const spy = spyOn(DataJobUtil, "isJobRunningPredicate").and.callThrough(); + const executions: DataJobExecutionDetails[] = [ + { status: DataJobExecutionStatusDeprecated.FAILED }, + { status: DataJobExecutionStatusDeprecated.PLATFORM_ERROR }, + { status: DataJobExecutionStatusDeprecated.SUCCEEDED }, + { status: DataJobExecutionStatusDeprecated.SKIPPED }, + ] as any; + + // When + DataJobUtil.isJobRunning(executions); + + // Then + expect(spy).toHaveBeenCalledTimes(4); + expect(spy.calls.argsFor(0)[0]).toEqual(executions[0]); + expect(spy.calls.argsFor(1)[0]).toEqual(executions[1]); + expect(spy.calls.argsFor(2)[0]).toEqual(executions[2]); + expect(spy.calls.argsFor(3)[0]).toEqual(executions[3]); + }); + }); + + describe("|convertFromExecutionDetailsToExecutionState|", () => { + let executionDetails: DataJobExecutionDetails; + let expectedExecution: DataJobExecution; + + beforeEach(() => { + executionDetails = { + id: "id001", + job_name: "job001", + type: "manual", + status: DataJobExecutionStatusDeprecated.SUBMITTED, + start_time: new Date().toISOString(), + started_by: "aUserov", + end_time: new Date().toISOString(), + op_id: "op001", + message: "message001", + logs_url: "http://url", + deployment: { + schedule: { + schedule_cron: "5 5 5 5 *", + }, + id: "id002", + enabled: true, + job_version: "002", + mode: "test_mode", + vdk_version: "002", + python_version: "3.9-secure", + resources: { + memory_limit: 1000, + memory_request: 1000, + cpu_limit: 0.5, + cpu_request: 0.5, + }, + deployed_date: "2020-11-11T10:10:10Z", + deployed_by: "pmitev", + }, + }; + expectedExecution = { + id: "id001", + jobName: "job001", + type: DataJobExecutionType.MANUAL, + status: DataJobExecutionStatus.SUBMITTED, + startTime: executionDetails.start_time, + startedBy: "aUserov", + endTime: executionDetails.end_time, + opId: "op001", + message: "message001", + logsUrl: "http://url", + deployment: { + schedule: { + scheduleCron: "5 5 5 5 *", + }, + id: "id002", + enabled: true, + jobVersion: "002", + mode: "test_mode", + vdkVersion: "002", + jobPythonVersion: "3.9-secure", + resources: { + memoryLimit: 1000, + memoryRequest: 1000, + cpuLimit: 0.5, + cpuRequest: 0.5, + }, + deployedDate: "2020-11-11T10:10:10Z", + deployedBy: "pmitev", + }, + }; + }); + + it("should verify will return empty execution when null and undefined provided", () => { + // When + const res1 = + DataJobUtil.convertFromExecutionDetailsToExecutionState(null); + const res2 = + DataJobUtil.convertFromExecutionDetailsToExecutionState(undefined); + + // Then + expect(res1).toEqual({ id: null }); + expect(res2).toEqual({ id: null }); + }); + + it("should verify will correctly convert case 1", () => { + // When + const converted = + DataJobUtil.convertFromExecutionDetailsToExecutionState( + executionDetails, + ); + + // Then + expect(converted).toEqual(expectedExecution); + }); + + it("should verify will correctly convert case 2", () => { + // Given + delete executionDetails.deployment.resources; + expectedExecution.deployment.resources = {} as any; + + // When + const converted = + DataJobUtil.convertFromExecutionDetailsToExecutionState( + executionDetails, + ); + + // Then + expect(converted).toEqual(expectedExecution); + }); + + it("should verify will correctly convert case 3", () => { + // Given + delete executionDetails.deployment.schedule; + expectedExecution.deployment.schedule = {} as any; + + // When + const converted = + DataJobUtil.convertFromExecutionDetailsToExecutionState( + executionDetails, + ); + + // Then + expect(converted).toEqual(expectedExecution); }); - describe('|convertFromExecutionDetailsToExecutionState|', () => { - let executionDetails: DataJobExecutionDetails; - let expectedExecution: DataJobExecution; - - beforeEach(() => { - executionDetails = { - id: 'id001', - job_name: 'job001', - type: 'manual', - status: DataJobExecutionStatusDeprecated.SUBMITTED, - start_time: new Date().toISOString(), - started_by: 'aUserov', - end_time: new Date().toISOString(), - op_id: 'op001', - message: 'message001', - logs_url: 'http://url', - deployment: { - schedule: { - schedule_cron: '5 5 5 5 *' - }, - id: 'id002', - enabled: true, - job_version: '002', - mode: 'test_mode', - vdk_version: '002', - python_version: '3.9-secure', - resources: { - memory_limit: 1000, - memory_request: 1000, - cpu_limit: 0.5, - cpu_request: 0.5 - }, - deployed_date: '2020-11-11T10:10:10Z', - deployed_by: 'pmitev' - } - }; - expectedExecution = { - id: 'id001', - jobName: 'job001', - type: DataJobExecutionType.MANUAL, - status: DataJobExecutionStatus.SUBMITTED, - startTime: executionDetails.start_time, - startedBy: 'aUserov', - endTime: executionDetails.end_time, - opId: 'op001', - message: 'message001', - logsUrl: 'http://url', - deployment: { - schedule: { - scheduleCron: '5 5 5 5 *' - }, - id: 'id002', - enabled: true, - jobVersion: '002', - mode: 'test_mode', - vdkVersion: '002', - jobPythonVersion: '3.9-secure', - resources: { - memoryLimit: 1000, - memoryRequest: 1000, - cpuLimit: 0.5, - cpuRequest: 0.5 - }, - deployedDate: '2020-11-11T10:10:10Z', - deployedBy: 'pmitev' - } - }; - }); - - it('should verify will return empty execution when null and undefined provided', () => { - // When - const res1 = DataJobUtil.convertFromExecutionDetailsToExecutionState(null); - const res2 = DataJobUtil.convertFromExecutionDetailsToExecutionState(undefined); - - // Then - expect(res1).toEqual({ id: null }); - expect(res2).toEqual({ id: null }); - }); - - it('should verify will correctly convert case 1', () => { - // When - const converted = DataJobUtil.convertFromExecutionDetailsToExecutionState(executionDetails); - - // Then - expect(converted).toEqual(expectedExecution); - }); - - it('should verify will correctly convert case 2', () => { - // Given - delete executionDetails.deployment.resources; - expectedExecution.deployment.resources = {} as any; - - // When - const converted = DataJobUtil.convertFromExecutionDetailsToExecutionState(executionDetails); - - // Then - expect(converted).toEqual(expectedExecution); - }); - - it('should verify will correctly convert case 3', () => { - // Given - delete executionDetails.deployment.schedule; - expectedExecution.deployment.schedule = {} as any; - - // When - const converted = DataJobUtil.convertFromExecutionDetailsToExecutionState(executionDetails); - - // Then - expect(converted).toEqual(expectedExecution); - }); - - it('should verify will correctly convert case 4', () => { - // Given - delete executionDetails.deployment; - expectedExecution.deployment = { - schedule: {}, - resources: {} - } as any; - - // When - const converted = DataJobUtil.convertFromExecutionDetailsToExecutionState(executionDetails); - - // Then - expect(converted).toEqual(expectedExecution); - }); + it("should verify will correctly convert case 4", () => { + // Given + delete executionDetails.deployment; + expectedExecution.deployment = { + schedule: {}, + resources: {}, + } as any; + + // When + const converted = + DataJobUtil.convertFromExecutionDetailsToExecutionState( + executionDetails, + ); + + // Then + expect(converted).toEqual(expectedExecution); }); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.ts index 11ff5625b0..e22040781a 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/data-job.util.ts @@ -3,87 +3,114 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; import { - DataJobDeployment, - DataJobExecution, - DataJobExecutionDetails, - DataJobExecutionStatus, - DataJobExecutionStatusDeprecated, - DataJobExecutionType -} from '../../model'; + DataJobDeployment, + DataJobExecution, + DataJobExecutionDetails, + DataJobExecutionStatus, + DataJobExecutionStatusDeprecated, + DataJobExecutionType, +} from "../../model"; /** * ** Utils for Data Job. */ export class DataJobUtil { - /** - * ** Predicate for Job Running. - */ - static isJobRunningPredicate = (jobExecution: DataJobExecution | DataJobExecutionDetails): boolean => { - return ( - (jobExecution as DataJobExecution).status === DataJobExecutionStatus.RUNNING || - (jobExecution as DataJobExecution).status === DataJobExecutionStatus.SUBMITTED || - (jobExecution as DataJobExecutionDetails).status === DataJobExecutionStatusDeprecated.RUNNING || - (jobExecution as DataJobExecutionDetails).status === DataJobExecutionStatusDeprecated.SUBMITTED - ); - }; - - /** - * ** Find if some Job is running in provided Executions. - */ - static isJobRunning(jobExecutions: DataJobExecution[] | DataJobExecutionDetails[]): boolean { - // eslint-disable-next-line @typescript-eslint/unbound-method - return jobExecutions.findIndex(DataJobUtil.isJobRunningPredicate) !== -1; - } + /** + * ** Predicate for Job Running. + */ + static isJobRunningPredicate = ( + jobExecution: DataJobExecution | DataJobExecutionDetails, + ): boolean => { + return ( + (jobExecution as DataJobExecution).status === + DataJobExecutionStatus.RUNNING || + (jobExecution as DataJobExecution).status === + DataJobExecutionStatus.SUBMITTED || + (jobExecution as DataJobExecutionDetails).status === + DataJobExecutionStatusDeprecated.RUNNING || + (jobExecution as DataJobExecutionDetails).status === + DataJobExecutionStatusDeprecated.SUBMITTED + ); + }; - static convertFromExecutionDetailsToExecutionState(jobExecutionDetails: DataJobExecutionDetails): DataJobExecution { - if (CollectionsUtil.isNil(jobExecutionDetails)) { - return { - id: null - }; - } + /** + * ** Find if some Job is running in provided Executions. + */ + static isJobRunning( + jobExecutions: DataJobExecution[] | DataJobExecutionDetails[], + ): boolean { + // eslint-disable-next-line @typescript-eslint/unbound-method + return jobExecutions.findIndex(DataJobUtil.isJobRunningPredicate) !== -1; + } - const execution: DataJobExecution = { - id: jobExecutionDetails.id, - jobName: jobExecutionDetails.job_name, - opId: jobExecutionDetails.op_id, - status: jobExecutionDetails.status.toUpperCase() as DataJobExecutionStatus, - startedBy: jobExecutionDetails.started_by, - startTime: jobExecutionDetails.start_time, - endTime: jobExecutionDetails.end_time, - message: jobExecutionDetails.message, - type: jobExecutionDetails.type.toUpperCase() as DataJobExecutionType, - logsUrl: jobExecutionDetails.logs_url, - deployment: { - schedule: {}, - resources: {} - } as DataJobDeployment - }; + static convertFromExecutionDetailsToExecutionState( + jobExecutionDetails: DataJobExecutionDetails, + ): DataJobExecution { + if (CollectionsUtil.isNil(jobExecutionDetails)) { + return { + id: null, + }; + } - if (CollectionsUtil.isLiteralObject(jobExecutionDetails.deployment)) { - execution.deployment.id = jobExecutionDetails.deployment.id; - execution.deployment.enabled = jobExecutionDetails.deployment.enabled; - execution.deployment.jobVersion = jobExecutionDetails.deployment.job_version; - execution.deployment.vdkVersion = jobExecutionDetails.deployment.vdk_version; - execution.deployment.mode = jobExecutionDetails.deployment.mode; - execution.deployment.deployedDate = jobExecutionDetails.deployment.deployed_date; - execution.deployment.deployedBy = jobExecutionDetails.deployment.deployed_by; - execution.deployment.jobPythonVersion = jobExecutionDetails.deployment.python_version; + const execution: DataJobExecution = { + id: jobExecutionDetails.id, + jobName: jobExecutionDetails.job_name, + opId: jobExecutionDetails.op_id, + status: + jobExecutionDetails.status.toUpperCase() as DataJobExecutionStatus, + startedBy: jobExecutionDetails.started_by, + startTime: jobExecutionDetails.start_time, + endTime: jobExecutionDetails.end_time, + message: jobExecutionDetails.message, + type: jobExecutionDetails.type.toUpperCase() as DataJobExecutionType, + logsUrl: jobExecutionDetails.logs_url, + deployment: { + schedule: {}, + resources: {}, + } as DataJobDeployment, + }; - if (CollectionsUtil.isLiteralObject(jobExecutionDetails.deployment.schedule)) { - execution.deployment.schedule.scheduleCron = jobExecutionDetails.deployment.schedule.schedule_cron; - } + if (CollectionsUtil.isLiteralObject(jobExecutionDetails.deployment)) { + execution.deployment.id = jobExecutionDetails.deployment.id; + execution.deployment.enabled = jobExecutionDetails.deployment.enabled; + execution.deployment.jobVersion = + jobExecutionDetails.deployment.job_version; + execution.deployment.vdkVersion = + jobExecutionDetails.deployment.vdk_version; + execution.deployment.mode = jobExecutionDetails.deployment.mode; + execution.deployment.deployedDate = + jobExecutionDetails.deployment.deployed_date; + execution.deployment.deployedBy = + jobExecutionDetails.deployment.deployed_by; + execution.deployment.jobPythonVersion = + jobExecutionDetails.deployment.python_version; - if (CollectionsUtil.isLiteralObject(jobExecutionDetails.deployment.resources)) { - execution.deployment.resources.cpuRequest = jobExecutionDetails.deployment.resources.cpu_request; - execution.deployment.resources.cpuLimit = jobExecutionDetails.deployment.resources.cpu_limit; - execution.deployment.resources.memoryRequest = jobExecutionDetails.deployment.resources.memory_request; - execution.deployment.resources.memoryLimit = jobExecutionDetails.deployment.resources.memory_limit; - } - } + if ( + CollectionsUtil.isLiteralObject(jobExecutionDetails.deployment.schedule) + ) { + execution.deployment.schedule.scheduleCron = + jobExecutionDetails.deployment.schedule.schedule_cron; + } - return execution; + if ( + CollectionsUtil.isLiteralObject( + jobExecutionDetails.deployment.resources, + ) + ) { + execution.deployment.resources.cpuRequest = + jobExecutionDetails.deployment.resources.cpu_request; + execution.deployment.resources.cpuLimit = + jobExecutionDetails.deployment.resources.cpu_limit; + execution.deployment.resources.memoryRequest = + jobExecutionDetails.deployment.resources.memory_request; + execution.deployment.resources.memoryLimit = + jobExecutionDetails.deployment.resources.memory_limit; + } } + + return execution; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.spec.ts index 144c767dd3..87b29e76a5 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.spec.ts @@ -3,50 +3,71 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataJobExecution, DataJobExecutionStatus, DataJobExecutionType } from '../../model'; +import { + DataJobExecution, + DataJobExecutionStatus, + DataJobExecutionType, +} from "../../model"; -import { DateUtil } from './date.util'; +import { DateUtil } from "./date.util"; const LESSER_DATE_JOBS_EXECUTION: DataJobExecution = { - id: 'oneId', - startTime: new Date(1).toISOString(), - jobName: 'oneJob', - status: DataJobExecutionStatus.SUBMITTED, - startedBy: '', - type: DataJobExecutionType.MANUAL, - endTime: new Date(1).toISOString(), - opId: 'oneOp', - message: 'oneMessage', - logsUrl: 'https::/logs.com' + id: "oneId", + startTime: new Date(1).toISOString(), + jobName: "oneJob", + status: DataJobExecutionStatus.SUBMITTED, + startedBy: "", + type: DataJobExecutionType.MANUAL, + endTime: new Date(1).toISOString(), + opId: "oneOp", + message: "oneMessage", + logsUrl: "https::/logs.com", }; const GREATER_DATE_JOBS_EXECUTION: DataJobExecution = { - id: 'twoId', - startTime: new Date(2).toISOString(), - jobName: 'twoJob', - status: DataJobExecutionStatus.SUBMITTED, - startedBy: '', - type: DataJobExecutionType.MANUAL, - endTime: new Date(2).toISOString(), - opId: 'twoOp', - message: 'twoMessage', - logsUrl: 'https::/logs.com' + id: "twoId", + startTime: new Date(2).toISOString(), + jobName: "twoJob", + status: DataJobExecutionStatus.SUBMITTED, + startedBy: "", + type: DataJobExecutionType.MANUAL, + endTime: new Date(2).toISOString(), + opId: "twoOp", + message: "twoMessage", + logsUrl: "https::/logs.com", }; -describe('DateUtil', () => { - it('Compare Dates Asc for equal left and right', () => { - expect(DateUtil.compareDatesAsc(LESSER_DATE_JOBS_EXECUTION, LESSER_DATE_JOBS_EXECUTION)).toEqual(0); - }); +describe("DateUtil", () => { + it("Compare Dates Asc for equal left and right", () => { + expect( + DateUtil.compareDatesAsc( + LESSER_DATE_JOBS_EXECUTION, + LESSER_DATE_JOBS_EXECUTION, + ), + ).toEqual(0); + }); - it('Compare Dates Asc for greater left', () => { - expect(DateUtil.compareDatesAsc(GREATER_DATE_JOBS_EXECUTION, LESSER_DATE_JOBS_EXECUTION)).toBeGreaterThan(0); - }); + it("Compare Dates Asc for greater left", () => { + expect( + DateUtil.compareDatesAsc( + GREATER_DATE_JOBS_EXECUTION, + LESSER_DATE_JOBS_EXECUTION, + ), + ).toBeGreaterThan(0); + }); - it('Compare Dates Asc for greater right', () => { - expect(DateUtil.compareDatesAsc(LESSER_DATE_JOBS_EXECUTION, GREATER_DATE_JOBS_EXECUTION)).toBeLessThan(0); - }); + it("Compare Dates Asc for greater right", () => { + expect( + DateUtil.compareDatesAsc( + LESSER_DATE_JOBS_EXECUTION, + GREATER_DATE_JOBS_EXECUTION, + ), + ).toBeLessThan(0); + }); - it('GetDateInUTC', () => { - expect(DateUtil.normalizeToUTC('2021-12-10T10:12:12Z').getHours()).toEqual(10); - }); + it("GetDateInUTC", () => { + expect(DateUtil.normalizeToUTC("2021-12-10T10:12:12Z").getHours()).toEqual( + 10, + ); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.ts index 17d555085c..225f78d95d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/date.util.ts @@ -3,17 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataJobExecution } from '../../model'; +import { DataJobExecution } from "../../model"; export class DateUtil { - static compareDatesAsc(left: DataJobExecution, right: DataJobExecution): number { - const leftStartTime = left.startTime ?? 0; - const rightStartTime = right.endTime ?? 0; + static compareDatesAsc( + left: DataJobExecution, + right: DataJobExecution, + ): number { + const leftStartTime = left.startTime ?? 0; + const rightStartTime = right.endTime ?? 0; - return new Date(leftStartTime).getTime() - new Date(rightStartTime).getTime(); - } + return ( + new Date(leftStartTime).getTime() - new Date(rightStartTime).getTime() + ); + } - static normalizeToUTC(dateISO: string): Date { - return new Date(dateISO.replace(/Z$/, '')); - } + static normalizeToUTC(dateISO: string): Date { + return new Date(dateISO.replace(/Z$/, "")); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/error.util.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/error.util.ts index 0d6f713640..4b023012b2 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/error.util.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/error.util.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ApolloError } from '@apollo/client/core'; +import { ApolloError } from "@apollo/client/core"; -import { CollectionsUtil } from '@versatiledatakit/shared'; +import { CollectionsUtil } from "@versatiledatakit/shared"; /** * ** Error Utils class. @@ -13,14 +13,17 @@ import { CollectionsUtil } from '@versatiledatakit/shared'; * @author gorankokin */ export class ErrorUtil { - /** - * ** Extract root Error depending of the format. - */ - static extractError(error: Error): Error { - if (error instanceof ApolloError && CollectionsUtil.isDefined(error.networkError)) { - return error.networkError; - } - - return error; + /** + * ** Extract root Error depending of the format. + */ + static extractError(error: Error): Error { + if ( + error instanceof ApolloError && + CollectionsUtil.isDefined(error.networkError) + ) { + return error.networkError; } + + return error; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/index.ts index e87b4d1bae..5609ce9d3c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/index.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './cron.util'; -export * from './data-job.util'; -export * from './date.util'; -export * from './error.util'; -export * from './string.util'; +export * from "./cron.util"; +export * from "./data-job.util"; +export * from "./date.util"; +export * from "./error.util"; +export * from "./string.util"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.spec.ts index ab2260f1d1..2b451fb896 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.spec.ts @@ -3,22 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { StringUtil } from './string.util'; +import { StringUtil } from "./string.util"; -describe('StringUtil', () => { - it('parsing string with single var', () => { - expect(StringUtil.stringFormat('Hello {0}', 'World!')).toBe('Hello World!'); - }); +describe("StringUtil", () => { + it("parsing string with single var", () => { + expect(StringUtil.stringFormat("Hello {0}", "World!")).toBe("Hello World!"); + }); - it('parsing string with multiple var', () => { - expect(StringUtil.stringFormat('Hello {0} {1}', 'beautiful', 'World!')).toBe('Hello beautiful World!'); - }); + it("parsing string with multiple var", () => { + expect( + StringUtil.stringFormat("Hello {0} {1}", "beautiful", "World!"), + ).toBe("Hello beautiful World!"); + }); - it('parsing string with multiple same var', () => { - expect(StringUtil.stringFormat('Hello {0} {0}', 'beautiful', 'World!')).toBe('Hello beautiful beautiful'); - }); + it("parsing string with multiple same var", () => { + expect( + StringUtil.stringFormat("Hello {0} {0}", "beautiful", "World!"), + ).toBe("Hello beautiful beautiful"); + }); - it('parsing string with same var', () => { - expect(StringUtil.stringFormat('Hello {0} {0}', 'beautiful')).toBe('Hello beautiful beautiful'); - }); + it("parsing string with same var", () => { + expect(StringUtil.stringFormat("Hello {0} {0}", "beautiful")).toBe( + "Hello beautiful beautiful", + ); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.ts index a2fe176855..4e30999e51 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/shared/utils/string.util.ts @@ -4,5 +4,6 @@ */ export class StringUtil { - static stringFormat = (str: string, ...args: string[]) => str.replace(/{(\d+)}/g, (match, index: number) => args[index] || ''); + static stringFormat = (str: string, ...args: string[]) => + str.replace(/{(\d+)}/g, (match, index: number) => args[index] || ""); } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/data-jobs.actions.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/data-jobs.actions.ts index 8ed500a397..6dcf874d77 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/data-jobs.actions.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/data-jobs.actions.ts @@ -6,19 +6,20 @@ /** * ** Action type Fetch Data Jobs. */ -export const FETCH_DATA_JOBS = '[feature::data-pipelines] Fetch Data Jobs'; +export const FETCH_DATA_JOBS = "[feature::data-pipelines] Fetch Data Jobs"; /** * ** Action type Fetch Data Job. */ -export const FETCH_DATA_JOB = '[feature::data-pipelines] Fetch Data Job'; +export const FETCH_DATA_JOB = "[feature::data-pipelines] Fetch Data Job"; /** * ** Action type Fetch Data Job executions. */ -export const FETCH_DATA_JOB_EXECUTIONS = '[feature::data-pipelines] Fetch Data Job Executions'; +export const FETCH_DATA_JOB_EXECUTIONS = + "[feature::data-pipelines] Fetch Data Job Executions"; /** * ** Action type Update Data job. */ -export const UPDATE_DATA_JOB = '[feature::data-pipelines] Update Data Job'; +export const UPDATE_DATA_JOB = "[feature::data-pipelines] Update Data Job"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/index.ts index 874cd271cd..7446b704a8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/actions/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs.actions'; +export * from "./data-jobs.actions"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.spec.ts index c379d1dfa5..22cf2974bf 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.spec.ts @@ -5,554 +5,617 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed, waitForAsync } from "@angular/core/testing"; -import { Observable, throwError } from 'rxjs'; +import { Observable, throwError } from "rxjs"; -import { marbles } from 'rxjs-marbles/jasmine'; +import { marbles } from "rxjs-marbles/jasmine"; -import { provideMockActions } from '@ngrx/effects/testing'; +import { provideMockActions } from "@ngrx/effects/testing"; -import { ApolloQueryResult } from '@apollo/client/core'; +import { ApolloQueryResult } from "@apollo/client/core"; import { - CollectionsUtil, - ComponentFailed, - ComponentLoaded, - ComponentModel, - ComponentService, - ComponentStateImpl, - ComponentUpdate, - ErrorRecord, - generateErrorCode, - generateErrorCodes, - GenericAction, - LOADED, - LOADING, - RouterState, - RouteState, - StatusType -} from '@versatiledatakit/shared'; - -import { DataJobsApiService } from '../../services'; + CollectionsUtil, + ComponentFailed, + ComponentLoaded, + ComponentModel, + ComponentService, + ComponentStateImpl, + ComponentUpdate, + ErrorRecord, + generateErrorCode, + generateErrorCodes, + GenericAction, + LOADED, + LOADING, + RouterState, + RouteState, + StatusType, +} from "@versatiledatakit/shared"; + +import { DataJobsApiService } from "../../services"; import { - DataJob, - DataJobDetails, - DataJobExecutionsPage, - DataJobPage, - JOB_DEPLOYMENT_ID_REQ_PARAM, - JOB_DETAILS_DATA_KEY, - JOB_EXECUTIONS_DATA_KEY, - JOB_NAME_REQ_PARAM, - JOB_STATE_DATA_KEY, - JOBS_DATA_KEY, - TEAM_NAME_REQ_PARAM -} from '../../model'; + DataJob, + DataJobDetails, + DataJobExecutionsPage, + DataJobPage, + JOB_DEPLOYMENT_ID_REQ_PARAM, + JOB_DETAILS_DATA_KEY, + JOB_EXECUTIONS_DATA_KEY, + JOB_NAME_REQ_PARAM, + JOB_STATE_DATA_KEY, + JOBS_DATA_KEY, + TEAM_NAME_REQ_PARAM, +} from "../../model"; -import { FETCH_DATA_JOB, FETCH_DATA_JOB_EXECUTIONS, FETCH_DATA_JOBS, UPDATE_DATA_JOB } from '../actions'; +import { + FETCH_DATA_JOB, + FETCH_DATA_JOB_EXECUTIONS, + FETCH_DATA_JOBS, + UPDATE_DATA_JOB, +} from "../actions"; -import { DataJobsEffects } from './data-jobs.effects'; +import { DataJobsEffects } from "./data-jobs.effects"; import { - TASK_LOAD_JOB_DETAILS, - TASK_LOAD_JOB_EXECUTIONS, - TASK_LOAD_JOB_STATE, - TASK_LOAD_JOBS_STATE, - TASK_UPDATE_JOB_DESCRIPTION, - TASK_UPDATE_JOB_STATUS -} from '../tasks'; - -describe('DataJobsEffects', () => { - let effects: DataJobsEffects; - - let actions$: Observable; - - let dataJobsApiServiceStub: jasmine.SpyObj; - let componentServiceStub: jasmine.SpyObj; - - beforeEach(waitForAsync(() => { - dataJobsApiServiceStub = jasmine.createSpyObj('dataJobsApiService', [ - 'getJobs', - 'getJob', - 'getJobDetails', - 'getJobExecutions', - 'updateDataJob', - 'updateDataJobStatus' - ]); - componentServiceStub = jasmine.createSpyObj('componentService', ['getModel']); - - generateErrorCodes(dataJobsApiServiceStub, [ - 'getJob', - 'getJobDetails', - 'getJobExecutions', - 'getJobs', - 'updateDataJobStatus', - 'updateDataJob' - ]); - - TestBed.configureTestingModule({ - providers: [ - { - provide: DataJobsApiService, - useValue: dataJobsApiServiceStub - }, - { - provide: ComponentService, - useValue: componentServiceStub - }, - provideMockActions(() => actions$), - DataJobsEffects - ] - }); - - effects = TestBed.inject(DataJobsEffects); - })); - - describe('Effects::', () => { - describe('|loadDataJobs$|', () => { - it( - 'should verify will load data-jobs', - marbles((m) => { - // Given - const dateNow = Date.now(); - spyOn(CollectionsUtil, 'dateNow').and.returnValue(dateNow); - const error = new Error('Something bad happened'); - const errorRecord: ErrorRecord = { - code: dataJobsApiServiceStub.errorCodes.getJobs.Unknown, - error, - objectUUID: effects.objectUUID - }; - - actions$ = m.cold('-a---------b', { - a: GenericAction.of(FETCH_DATA_JOBS, createModel().getComponentState()), - b: GenericAction.of(FETCH_DATA_JOBS, createModel(null, 4).getComponentState()) - }); - - let cntComponentServiceStub = 0; - componentServiceStub.getModel.and.callFake(() => { - cntComponentServiceStub++; - - if (cntComponentServiceStub === 1) { - return m.cold('--a', { a: createModel() }); - } - - return m.cold('--a', { a: createModel(null, 4) }); - }); - - const result = { - data: { - content: [{ jobName: 'job1' }, { jobName: 'job2' }, { jobName: 'job3' }], - totalPages: 0, - totalItems: 0 - } - } as ApolloQueryResult; - - let cntDataJobsApiServiceStub = 0; - dataJobsApiServiceStub.getJobs.and.callFake(() => { - cntDataJobsApiServiceStub++; - - if (cntDataJobsApiServiceStub === 1) { - return m.cold('-----a', { - a: result - }); - } - - return throwError(() => new Error('Something bad happened')); - }); - - const expected$ = m.cold('--------a------b', { - a: ComponentLoaded.of( - createModel() - .withData(JOBS_DATA_KEY, result.data) - .withTask(TASK_LOAD_JOBS_STATE) - .withStatusLoaded() - .getComponentState() - ), - b: ComponentFailed.of( - createModel(null, 4) - .withData(JOBS_DATA_KEY, { - content: [], - totalItems: 0, - totalPages: 0 - } as DataJobPage) - .withError(errorRecord) - .withTask(TASK_LOAD_JOBS_STATE) - .withStatusFailed() - .getComponentState() - ) - }); - - // When - const response$ = effects.loadDataJobs$; - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - }); - - describe('|loadDataJob$|', () => { - it( - 'should verify will load data-job', - marbles((m) => { - // Given - actions$ = m.cold('-a----------', { - a: GenericAction.of(FETCH_DATA_JOB, createModel().getComponentState()) - }); - - let cntComponentServiceStub = 0; - componentServiceStub.getModel.and.callFake(() => { - cntComponentServiceStub++; - - switch (cntComponentServiceStub) { - case 1: - return m.cold('--a', { a: createModel() }); - case 2: - return m.cold('---a', { a: createModel() }); - case 3: - return m.cold('---a', { - a: createModel(new Map([getJobStateTuple()]), 1, LOADED) - }); - case 4: - return m.cold('---a', { - a: createModel(new Map([getJobStateTuple(), getDataJobDetailsTuple()]), 1, LOADED) - }); - default: - return m.cold('---a', { - a: createModel( - new Map([ - getJobStateTuple(), - getDataJobDetailsTuple(), - [getExecutionsTuples()[0], getExecutionsTuples()[1].content] - ]), - 1, - LOADED - ) - }); - } - }); - - dataJobsApiServiceStub.getJob.and.returnValue( - m.cold('--a', { - a: getJobStateTuple()[1] - }) - ); - - dataJobsApiServiceStub.getJobDetails.and.returnValue( - m.cold('----a', { - a: getDataJobDetailsTuple()[1] - }) - ); - - dataJobsApiServiceStub.getJobExecutions.and.returnValue( - m.cold('------a', { - a: getExecutionsTuples()[1] - }) - ); - - const expected$ = m.cold('--------a-b-c', { - a: ComponentLoaded.of( - /* eslint-disable @typescript-eslint/no-unsafe-argument */ - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - /* eslint-disable @typescript-eslint/no-unsafe-call */ - createModel() - .clearErrors() - .withTask(TASK_LOAD_JOB_STATE) - .withData(JOB_STATE_DATA_KEY, getJobStateTuple()[1]) - .withStatusLoaded() - .getComponentState() - ), - b: ComponentUpdate.of( - createModel() - .clearErrors() - .withTask(TASK_LOAD_JOB_DETAILS) - .withData(JOB_STATE_DATA_KEY, getJobStateTuple()[1]) - .withData(JOB_DETAILS_DATA_KEY, getDataJobDetailsTuple()[1]) - .withStatusLoaded() - .getComponentState() - ), - c: ComponentUpdate.of( - createModel() - .clearErrors() - .withTask(TASK_LOAD_JOB_EXECUTIONS) - .withData(JOB_STATE_DATA_KEY, getJobStateTuple()[1]) - .withData(JOB_DETAILS_DATA_KEY, getDataJobDetailsTuple()[1]) - .withData(JOB_EXECUTIONS_DATA_KEY, getExecutionsTuples()[1].content) - .withStatusLoaded() - .getComponentState() - ) - }); - - // When - const response$ = effects.loadDataJob$; - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - }); - - describe('|loadDataJobExecutions$|', () => { - it( - 'should verify will load data-job executions', - marbles((m) => { - // Given - const genericAction = GenericAction.of(FETCH_DATA_JOB_EXECUTIONS, createModel().getComponentState()); - actions$ = m.cold('-a----------', { - a: genericAction - }); - - let cntComponentServiceStub = 0; - componentServiceStub.getModel.and.callFake(() => { - cntComponentServiceStub++; - - switch (cntComponentServiceStub) { - case 1: - return m.cold('--a', { a: createModel() }); - case 2: - return m.cold('---a', { a: createModel() }); - default: - return m.cold('---a', { - a: createModel( - new Map([[getExecutionsTuples()[0], getExecutionsTuples()[1].content]]), - 1, - LOADED - ) - }); - } - }); - - dataJobsApiServiceStub.getJobExecutions.and.returnValue( - m.cold('--a', { - a: getExecutionsTuples()[1] - }) - ); - - const expected$ = m.cold('--------a', { - a: ComponentLoaded.of( - createModel() - .clearErrors() - .withTask(TASK_LOAD_JOB_EXECUTIONS) - .withData(JOB_EXECUTIONS_DATA_KEY, getExecutionsTuples()[1].content) - .withStatusLoaded() - .getComponentState() - ) - }); - - // When - const response$ = effects.loadDataJobExecutions$; - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - }); - - describe('|updateDataJob$|', () => { - it( - 'should verify will update data-job description', - marbles((m) => { - // Given - const genericAction1 = GenericAction.of( - UPDATE_DATA_JOB, - createModel(undefined, 1, LOADING).getComponentState(), - TASK_UPDATE_JOB_DESCRIPTION - ); - actions$ = m.cold('-a----------', { - a: genericAction1 - }); - - componentServiceStub.getModel.and.callFake(() => - m.cold('---a', { - a: createModel(undefined, 1, LOADING) - }) - ); - - dataJobsApiServiceStub.updateDataJob.and.returnValue( - m.cold('-----a', { - a: getDataJobDetailsTuple()[1] - }) - ); - - const expected$ = m.cold('---------a-', { - a: ComponentLoaded.of( - createModel(undefined, 1, LOADING) - .clearErrors() - .withTask(genericAction1.task) - .withData(JOB_DETAILS_DATA_KEY, undefined) - .withStatusLoaded() - .getComponentState() - ) - }); - - // When - const response$ = effects.updateDataJob$; - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - - it( - 'should verify will update data-job status', - marbles((m) => { - // Given - const genericAction1 = GenericAction.of( - UPDATE_DATA_JOB, - createModel(undefined, 1, LOADING).getComponentState(), - TASK_UPDATE_JOB_STATUS - ); - actions$ = m.cold('-a----------', { - a: genericAction1 - }); - - componentServiceStub.getModel.and.callFake(() => - m.cold('---a', { - a: createModel(undefined, 1, LOADING) - }) - ); - - dataJobsApiServiceStub.updateDataJobStatus.and.returnValue( - m.cold('-----a', { - a: { enabled: true } - }) - ); - - const expected$ = m.cold('---------a-', { - a: ComponentLoaded.of( - createModel(undefined, 1, LOADING) - .clearErrors() - .withTask(genericAction1.task) - .withData(JOB_STATE_DATA_KEY, undefined) - .withStatusLoaded() - .getComponentState() - ) - }); - - // When - const response$ = effects.updateDataJob$; - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - - it( - 'should verify will fail', - marbles((m) => { - // Given - const dateNow = Date.now(); - spyOn(CollectionsUtil, 'dateNow').and.returnValue(dateNow); - const error = new Error('Unsupported action task for Data Pipelines, update Data Job.'); - const errorRecord: ErrorRecord = { - code: generateErrorCode( - DataJobsEffects.CLASS_NAME, - DataJobsEffects.PUBLIC_NAME, - '_updateJob', - 'UnsupportedActionTask' - ), - error, - objectUUID: effects.objectUUID - }; - const genericAction1 = GenericAction.of( - UPDATE_DATA_JOB, - createModel(undefined, 1, LOADING).getComponentState(), - TASK_LOAD_JOB_STATE - ); - actions$ = m.cold('-a----------', { - a: genericAction1 - }); - - componentServiceStub.getModel.and.callFake(() => - m.cold('---a', { - a: createModel() - }) - ); - - const expected$ = m.cold('----a-', { - a: ComponentFailed.of( - createModel().withTask(genericAction1.task).withError(errorRecord).withStatusFailed().getComponentState() - ) - }); - - // When - const response$ = effects.updateDataJob$; - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - }); + TASK_LOAD_JOB_DETAILS, + TASK_LOAD_JOB_EXECUTIONS, + TASK_LOAD_JOB_STATE, + TASK_LOAD_JOBS_STATE, + TASK_UPDATE_JOB_DESCRIPTION, + TASK_UPDATE_JOB_STATUS, +} from "../tasks"; + +describe("DataJobsEffects", () => { + let effects: DataJobsEffects; + + let actions$: Observable; + + let dataJobsApiServiceStub: jasmine.SpyObj; + let componentServiceStub: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + dataJobsApiServiceStub = jasmine.createSpyObj( + "dataJobsApiService", + [ + "getJobs", + "getJob", + "getJobDetails", + "getJobExecutions", + "updateDataJob", + "updateDataJobStatus", + ], + ); + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["getModel"], + ); + + generateErrorCodes(dataJobsApiServiceStub, [ + "getJob", + "getJobDetails", + "getJobExecutions", + "getJobs", + "updateDataJobStatus", + "updateDataJob", + ]); + + TestBed.configureTestingModule({ + providers: [ + { + provide: DataJobsApiService, + useValue: dataJobsApiServiceStub, + }, + { + provide: ComponentService, + useValue: componentServiceStub, + }, + provideMockActions(() => actions$), + DataJobsEffects, + ], }); -}); -const createModel = (data?: Map, navigationId = 1, status: StatusType = LOADING, requestParams: Array<[string, any]> = []) => - ComponentModel.of( - ComponentStateImpl.of({ - id: 'testComponent', - status, - data, - requestParams: new Map([ - [TEAM_NAME_REQ_PARAM, 'aTeam'], - [JOB_NAME_REQ_PARAM, 'aJob'], - [JOB_DEPLOYMENT_ID_REQ_PARAM, 'aJobDeploymentId'], - ...requestParams - ]), - navigationId + effects = TestBed.inject(DataJobsEffects); + })); + + describe("Effects::", () => { + describe("|loadDataJobs$|", () => { + it( + "should verify will load data-jobs", + marbles((m) => { + // Given + const dateNow = Date.now(); + spyOn(CollectionsUtil, "dateNow").and.returnValue(dateNow); + const error = new Error("Something bad happened"); + const errorRecord: ErrorRecord = { + code: dataJobsApiServiceStub.errorCodes.getJobs.Unknown, + error, + objectUUID: effects.objectUUID, + }; + + actions$ = m.cold("-a---------b", { + a: GenericAction.of( + FETCH_DATA_JOBS, + createModel().getComponentState(), + ), + b: GenericAction.of( + FETCH_DATA_JOBS, + createModel(null, 4).getComponentState(), + ), + }); + + let cntComponentServiceStub = 0; + componentServiceStub.getModel.and.callFake(() => { + cntComponentServiceStub++; + + if (cntComponentServiceStub === 1) { + return m.cold("--a", { a: createModel() }); + } + + return m.cold("--a", { a: createModel(null, 4) }); + }); + + const result = { + data: { + content: [ + { jobName: "job1" }, + { jobName: "job2" }, + { jobName: "job3" }, + ], + totalPages: 0, + totalItems: 0, + }, + } as ApolloQueryResult; + + let cntDataJobsApiServiceStub = 0; + dataJobsApiServiceStub.getJobs.and.callFake(() => { + cntDataJobsApiServiceStub++; + + if (cntDataJobsApiServiceStub === 1) { + return m.cold("-----a", { + a: result, + }); + } + + return throwError(() => new Error("Something bad happened")); + }); + + const expected$ = m.cold("--------a------b", { + a: ComponentLoaded.of( + createModel() + .withData(JOBS_DATA_KEY, result.data) + .withTask(TASK_LOAD_JOBS_STATE) + .withStatusLoaded() + .getComponentState(), + ), + b: ComponentFailed.of( + createModel(null, 4) + .withData(JOBS_DATA_KEY, { + content: [], + totalItems: 0, + totalPages: 0, + } as DataJobPage) + .withError(errorRecord) + .withTask(TASK_LOAD_JOBS_STATE) + .withStatusFailed() + .getComponentState(), + ), + }); + + // When + const response$ = effects.loadDataJobs$; + + // Then + m.expect(response$).toBeObservable(expected$); }), - RouterState.of(RouteState.empty(), navigationId) - ); + ); + }); -const getJobStateTuple = () => - [ - JOB_STATE_DATA_KEY, - { - jobName: 'aJob', - deployments: [], - config: { - schedule: { - scheduleCron: '5 5 5 5 *' - }, - description: 'aDesc', - team: 'aTeam', - logsUrl: 'http://url', - sourceUrl: 'http://urlsource' + describe("|loadDataJob$|", () => { + it( + "should verify will load data-job", + marbles((m) => { + // Given + actions$ = m.cold("-a----------", { + a: GenericAction.of( + FETCH_DATA_JOB, + createModel().getComponentState(), + ), + }); + + let cntComponentServiceStub = 0; + componentServiceStub.getModel.and.callFake(() => { + cntComponentServiceStub++; + + switch (cntComponentServiceStub) { + case 1: + return m.cold("--a", { a: createModel() }); + case 2: + return m.cold("---a", { a: createModel() }); + case 3: + return m.cold("---a", { + a: createModel( + new Map([getJobStateTuple()]), + 1, + LOADED, + ), + }); + case 4: + return m.cold("---a", { + a: createModel( + new Map([ + getJobStateTuple(), + getDataJobDetailsTuple(), + ]), + 1, + LOADED, + ), + }); + default: + return m.cold("---a", { + a: createModel( + new Map([ + getJobStateTuple(), + getDataJobDetailsTuple(), + [ + getExecutionsTuples()[0], + getExecutionsTuples()[1].content, + ], + ]), + 1, + LOADED, + ), + }); } - } - ] as [string, DataJob]; + }); + + dataJobsApiServiceStub.getJob.and.returnValue( + m.cold("--a", { + a: getJobStateTuple()[1], + }), + ); + + dataJobsApiServiceStub.getJobDetails.and.returnValue( + m.cold("----a", { + a: getDataJobDetailsTuple()[1], + }), + ); + + dataJobsApiServiceStub.getJobExecutions.and.returnValue( + m.cold("------a", { + a: getExecutionsTuples()[1], + }), + ); + + const expected$ = m.cold("--------a-b-c", { + a: ComponentLoaded.of( + /* eslint-disable @typescript-eslint/no-unsafe-argument */ + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + /* eslint-disable @typescript-eslint/no-unsafe-call */ + createModel() + .clearErrors() + .withTask(TASK_LOAD_JOB_STATE) + .withData(JOB_STATE_DATA_KEY, getJobStateTuple()[1]) + .withStatusLoaded() + .getComponentState(), + ), + b: ComponentUpdate.of( + createModel() + .clearErrors() + .withTask(TASK_LOAD_JOB_DETAILS) + .withData(JOB_STATE_DATA_KEY, getJobStateTuple()[1]) + .withData(JOB_DETAILS_DATA_KEY, getDataJobDetailsTuple()[1]) + .withStatusLoaded() + .getComponentState(), + ), + c: ComponentUpdate.of( + createModel() + .clearErrors() + .withTask(TASK_LOAD_JOB_EXECUTIONS) + .withData(JOB_STATE_DATA_KEY, getJobStateTuple()[1]) + .withData(JOB_DETAILS_DATA_KEY, getDataJobDetailsTuple()[1]) + .withData( + JOB_EXECUTIONS_DATA_KEY, + getExecutionsTuples()[1].content, + ) + .withStatusLoaded() + .getComponentState(), + ), + }); + + // When + const response$ = effects.loadDataJob$; + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + }); -const getDataJobDetailsTuple = () => - [ - JOB_DETAILS_DATA_KEY, - { - job_name: 'aJob', - team: 'aTeam', - description: 'aDesc', - config: { - schedule: { - schedule_cron: '5 5 5 5 *' - }, - contacts: { - notified_on_job_success: [], - notified_on_job_failure_user_error: [], - notified_on_job_failure_platform_error: [], - notified_on_job_deploy: [] - } + describe("|loadDataJobExecutions$|", () => { + it( + "should verify will load data-job executions", + marbles((m) => { + // Given + const genericAction = GenericAction.of( + FETCH_DATA_JOB_EXECUTIONS, + createModel().getComponentState(), + ); + actions$ = m.cold("-a----------", { + a: genericAction, + }); + + let cntComponentServiceStub = 0; + componentServiceStub.getModel.and.callFake(() => { + cntComponentServiceStub++; + + switch (cntComponentServiceStub) { + case 1: + return m.cold("--a", { a: createModel() }); + case 2: + return m.cold("---a", { a: createModel() }); + default: + return m.cold("---a", { + a: createModel( + new Map([ + [ + getExecutionsTuples()[0], + getExecutionsTuples()[1].content, + ], + ]), + 1, + LOADED, + ), + }); } - } - ] as [string, DataJobDetails]; + }); + + dataJobsApiServiceStub.getJobExecutions.and.returnValue( + m.cold("--a", { + a: getExecutionsTuples()[1], + }), + ); + + const expected$ = m.cold("--------a", { + a: ComponentLoaded.of( + createModel() + .clearErrors() + .withTask(TASK_LOAD_JOB_EXECUTIONS) + .withData( + JOB_EXECUTIONS_DATA_KEY, + getExecutionsTuples()[1].content, + ) + .withStatusLoaded() + .getComponentState(), + ), + }); + + // When + const response$ = effects.loadDataJobExecutions$; + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + }); + + describe("|updateDataJob$|", () => { + it( + "should verify will update data-job description", + marbles((m) => { + // Given + const genericAction1 = GenericAction.of( + UPDATE_DATA_JOB, + createModel(undefined, 1, LOADING).getComponentState(), + TASK_UPDATE_JOB_DESCRIPTION, + ); + actions$ = m.cold("-a----------", { + a: genericAction1, + }); + + componentServiceStub.getModel.and.callFake(() => + m.cold("---a", { + a: createModel(undefined, 1, LOADING), + }), + ); + + dataJobsApiServiceStub.updateDataJob.and.returnValue( + m.cold("-----a", { + a: getDataJobDetailsTuple()[1], + }), + ); + + const expected$ = m.cold("---------a-", { + a: ComponentLoaded.of( + createModel(undefined, 1, LOADING) + .clearErrors() + .withTask(genericAction1.task) + .withData(JOB_DETAILS_DATA_KEY, undefined) + .withStatusLoaded() + .getComponentState(), + ), + }); + + // When + const response$ = effects.updateDataJob$; + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + + it( + "should verify will update data-job status", + marbles((m) => { + // Given + const genericAction1 = GenericAction.of( + UPDATE_DATA_JOB, + createModel(undefined, 1, LOADING).getComponentState(), + TASK_UPDATE_JOB_STATUS, + ); + actions$ = m.cold("-a----------", { + a: genericAction1, + }); + + componentServiceStub.getModel.and.callFake(() => + m.cold("---a", { + a: createModel(undefined, 1, LOADING), + }), + ); + + dataJobsApiServiceStub.updateDataJobStatus.and.returnValue( + m.cold("-----a", { + a: { enabled: true }, + }), + ); + + const expected$ = m.cold("---------a-", { + a: ComponentLoaded.of( + createModel(undefined, 1, LOADING) + .clearErrors() + .withTask(genericAction1.task) + .withData(JOB_STATE_DATA_KEY, undefined) + .withStatusLoaded() + .getComponentState(), + ), + }); + + // When + const response$ = effects.updateDataJob$; + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + + it( + "should verify will fail", + marbles((m) => { + // Given + const dateNow = Date.now(); + spyOn(CollectionsUtil, "dateNow").and.returnValue(dateNow); + const error = new Error( + "Unsupported action task for Data Pipelines, update Data Job.", + ); + const errorRecord: ErrorRecord = { + code: generateErrorCode( + DataJobsEffects.CLASS_NAME, + DataJobsEffects.PUBLIC_NAME, + "_updateJob", + "UnsupportedActionTask", + ), + error, + objectUUID: effects.objectUUID, + }; + const genericAction1 = GenericAction.of( + UPDATE_DATA_JOB, + createModel(undefined, 1, LOADING).getComponentState(), + TASK_LOAD_JOB_STATE, + ); + actions$ = m.cold("-a----------", { + a: genericAction1, + }); + + componentServiceStub.getModel.and.callFake(() => + m.cold("---a", { + a: createModel(), + }), + ); + + const expected$ = m.cold("----a-", { + a: ComponentFailed.of( + createModel() + .withTask(genericAction1.task) + .withError(errorRecord) + .withStatusFailed() + .getComponentState(), + ), + }); + + // When + const response$ = effects.updateDataJob$; + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + }); + }); +}); + +const createModel = ( + data?: Map, + navigationId = 1, + status: StatusType = LOADING, + requestParams: Array<[string, any]> = [], +) => + ComponentModel.of( + ComponentStateImpl.of({ + id: "testComponent", + status, + data, + requestParams: new Map([ + [TEAM_NAME_REQ_PARAM, "aTeam"], + [JOB_NAME_REQ_PARAM, "aJob"], + [JOB_DEPLOYMENT_ID_REQ_PARAM, "aJobDeploymentId"], + ...requestParams, + ]), + navigationId, + }), + RouterState.of(RouteState.empty(), navigationId), + ); + +const getJobStateTuple = () => + [ + JOB_STATE_DATA_KEY, + { + jobName: "aJob", + deployments: [], + config: { + schedule: { + scheduleCron: "5 5 5 5 *", + }, + description: "aDesc", + team: "aTeam", + logsUrl: "http://url", + sourceUrl: "http://urlsource", + }, + }, + ] as [string, DataJob]; + +const getDataJobDetailsTuple = () => + [ + JOB_DETAILS_DATA_KEY, + { + job_name: "aJob", + team: "aTeam", + description: "aDesc", + config: { + schedule: { + schedule_cron: "5 5 5 5 *", + }, + contacts: { + notified_on_job_success: [], + notified_on_job_failure_user_error: [], + notified_on_job_failure_platform_error: [], + notified_on_job_deploy: [], + }, + }, + }, + ] as [string, DataJobDetails]; const getExecutionsTuples = () => - [ - JOB_EXECUTIONS_DATA_KEY, - { - content: [ - { jobName: 'aJob', logsUrl: 'http://a1', id: 'a1' }, - { jobName: 'aJob', logsUrl: 'http://a2', id: 'a2' }, - { jobName: 'aJob', logsUrl: 'http://a3', id: 'a3' }, - { jobName: 'aJob', logsUrl: 'http://a4', id: 'a4' } - ], - totalPages: 1, - totalItems: 4 - } - ] as [string, DataJobExecutionsPage]; + [ + JOB_EXECUTIONS_DATA_KEY, + { + content: [ + { jobName: "aJob", logsUrl: "http://a1", id: "a1" }, + { jobName: "aJob", logsUrl: "http://a2", id: "a2" }, + { jobName: "aJob", logsUrl: "http://a3", id: "a3" }, + { jobName: "aJob", logsUrl: "http://a4", id: "a4" }, + ], + totalPages: 1, + totalItems: 4, + }, + ] as [string, DataJobExecutionsPage]; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.ts index 0798806c06..eafbc94a60 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/data-jobs.effects.ts @@ -3,415 +3,504 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; -import { merge, Observable, of, throwError } from 'rxjs'; -import { catchError, map, switchMap, take, tap } from 'rxjs/operators'; +import { merge, Observable, of, throwError } from "rxjs"; +import { catchError, map, switchMap, take, tap } from "rxjs/operators"; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Actions, createEffect, ofType } from "@ngrx/effects"; import { - CollectionsUtil, - ComponentFailed, - ComponentLoaded, - ComponentModel, - ComponentService, - ComponentState, - ComponentUpdate, - extractTaskFromIdentifier, - generateErrorCode, - getModel, - getModelAndTask, - LOADED, - processServiceRequestError, - ServiceHttpErrorCodes, - StatusType, - TaurusBaseEffects -} from '@versatiledatakit/shared'; - -import { ErrorUtil } from '../../shared/utils'; + CollectionsUtil, + ComponentFailed, + ComponentLoaded, + ComponentModel, + ComponentService, + ComponentState, + ComponentUpdate, + extractTaskFromIdentifier, + generateErrorCode, + getModel, + getModelAndTask, + LOADED, + processServiceRequestError, + ServiceHttpErrorCodes, + StatusType, + TaurusBaseEffects, +} from "@versatiledatakit/shared"; + +import { ErrorUtil } from "../../shared/utils"; import { - DataJob, - DataJobDetails, - DataJobExecutionFilter, - DataJobExecutionOrder, - DataJobPage, - FILTER_REQ_PARAM, - JOB_DEPLOYMENT_ID_REQ_PARAM, - JOB_DETAILS_DATA_KEY, - JOB_DETAILS_REQ_PARAM, - JOB_EXECUTIONS_DATA_KEY, - JOB_NAME_REQ_PARAM, - JOB_STATE_DATA_KEY, - JOB_STATE_REQ_PARAM, - JOB_STATUS_REQ_PARAM, - JOBS_DATA_KEY, - ORDER_REQ_PARAM, - TEAM_NAME_REQ_PARAM -} from '../../model'; + DataJob, + DataJobDetails, + DataJobExecutionFilter, + DataJobExecutionOrder, + DataJobPage, + FILTER_REQ_PARAM, + JOB_DEPLOYMENT_ID_REQ_PARAM, + JOB_DETAILS_DATA_KEY, + JOB_DETAILS_REQ_PARAM, + JOB_EXECUTIONS_DATA_KEY, + JOB_NAME_REQ_PARAM, + JOB_STATE_DATA_KEY, + JOB_STATE_REQ_PARAM, + JOB_STATUS_REQ_PARAM, + JOBS_DATA_KEY, + ORDER_REQ_PARAM, + TEAM_NAME_REQ_PARAM, +} from "../../model"; import { - DataJobLoadTasks, - DataJobsLoadTasks, - DataJobUpdateTasks, - TASK_LOAD_JOB_DETAILS, - TASK_LOAD_JOB_EXECUTIONS, - TASK_LOAD_JOB_STATE, - TASK_LOAD_JOBS_STATE, - TASK_UPDATE_JOB_DESCRIPTION, - TASK_UPDATE_JOB_STATUS -} from '../tasks'; + DataJobLoadTasks, + DataJobsLoadTasks, + DataJobUpdateTasks, + TASK_LOAD_JOB_DETAILS, + TASK_LOAD_JOB_EXECUTIONS, + TASK_LOAD_JOB_STATE, + TASK_LOAD_JOBS_STATE, + TASK_UPDATE_JOB_DESCRIPTION, + TASK_UPDATE_JOB_STATUS, +} from "../tasks"; -import { LOAD_JOB_ERROR_CODES, LOAD_JOBS_ERROR_CODES, UPDATE_JOB_DETAILS_ERROR_CODES } from '../error-codes'; +import { + LOAD_JOB_ERROR_CODES, + LOAD_JOBS_ERROR_CODES, + UPDATE_JOB_DETAILS_ERROR_CODES, +} from "../error-codes"; -import { FETCH_DATA_JOB, FETCH_DATA_JOB_EXECUTIONS, FETCH_DATA_JOBS, UPDATE_DATA_JOB } from '../actions'; +import { + FETCH_DATA_JOB, + FETCH_DATA_JOB_EXECUTIONS, + FETCH_DATA_JOBS, + UPDATE_DATA_JOB, +} from "../actions"; -import { DataJobsApiService } from '../../services'; +import { DataJobsApiService } from "../../services"; /** * ** Effect for DataJobs. */ @Injectable() export class DataJobsEffects extends TaurusBaseEffects { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME = 'DataJobsEffects'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME = 'Data-Jobs-Effects'; - - /** - * ** Load DataJobs data. - */ - loadDataJobs$ = createEffect(() => - this.actions$.pipe( - ofType(FETCH_DATA_JOBS), - getModel(this.componentService), - switchMap((model: ComponentModel) => this._loadDataJobs(model)) - ) - ); - - loadDataJob$ = createEffect(() => - this.actions$.pipe( - ofType(FETCH_DATA_JOB), - getModel(this.componentService), - switchMap((model) => - merge( - this._executeJobTask(model, TASK_LOAD_JOB_STATE), - this._executeJobTask(model, TASK_LOAD_JOB_DETAILS), - this._executeJobTask(model, TASK_LOAD_JOB_EXECUTIONS) + /** + * @inheritDoc + */ + static override readonly CLASS_NAME = "DataJobsEffects"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME = "Data-Jobs-Effects"; + + /** + * ** Load DataJobs data. + */ + loadDataJobs$ = createEffect(() => + this.actions$.pipe( + ofType(FETCH_DATA_JOBS), + getModel(this.componentService), + switchMap((model: ComponentModel) => this._loadDataJobs(model)), + ), + ); + + loadDataJob$ = createEffect(() => + this.actions$.pipe( + ofType(FETCH_DATA_JOB), + getModel(this.componentService), + switchMap((model) => + merge( + this._executeJobTask(model, TASK_LOAD_JOB_STATE), + this._executeJobTask(model, TASK_LOAD_JOB_DETAILS), + this._executeJobTask(model, TASK_LOAD_JOB_EXECUTIONS), + ), + ), + ), + ); + + loadDataJobExecutions$ = createEffect(() => + this.actions$.pipe( + ofType(FETCH_DATA_JOB_EXECUTIONS), + getModel(this.componentService), + switchMap((model) => + this._executeJobTask(model, TASK_LOAD_JOB_EXECUTIONS), + ), + ), + ); + + updateDataJob$ = createEffect(() => + this.actions$.pipe( + ofType(UPDATE_DATA_JOB), + getModelAndTask(this.componentService), + switchMap(([model, task]) => this._updateJob(model, task)), // eslint-disable-line rxjs/no-unsafe-switchmap + ), + ); + + /** + * ** Constructor. + */ + constructor( + actions$: Actions, + componentService: ComponentService, + private readonly dataJobsApiService: DataJobsApiService, + ) { + super(actions$, componentService, DataJobsEffects.CLASS_NAME); + + this.registerEffectsErrorCodes(); + } + + /** + * @inheritDoc + * @protected + */ + protected registerEffectsErrorCodes(): void { + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE] = + this.dataJobsApiService.errorCodes.getJob; + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS] = + this.dataJobsApiService.errorCodes.getJobDetails; + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS] = + this.dataJobsApiService.errorCodes.getJobExecutions; + + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE] = + this.dataJobsApiService.errorCodes.getJobs; + + UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_STATUS] = + this.dataJobsApiService.errorCodes.updateDataJobStatus; + UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_DESCRIPTION] = + this.dataJobsApiService.errorCodes.updateDataJob; + } + + private _loadDataJobs( + componentModel: ComponentModel, + ): Observable { + const componentState = componentModel.getComponentState(); + const task: DataJobsLoadTasks = TASK_LOAD_JOBS_STATE; + + return of(componentModel).pipe( + switchMap((model) => + this.dataJobsApiService + .getJobs( + componentState.filter.criteria, + componentState.search, + componentState.page.page, + componentState.page.size, + ) + .pipe( + map((response) => + model + .clearTask() + .removeErrorCodePatterns( + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE].All, ) - ) - ) - ); - - loadDataJobExecutions$ = createEffect(() => - this.actions$.pipe( - ofType(FETCH_DATA_JOB_EXECUTIONS), - getModel(this.componentService), - switchMap((model) => this._executeJobTask(model, TASK_LOAD_JOB_EXECUTIONS)) - ) - ); - - updateDataJob$ = createEffect(() => - this.actions$.pipe( - ofType(UPDATE_DATA_JOB), - getModelAndTask(this.componentService), - switchMap(([model, task]) => this._updateJob(model, task)) // eslint-disable-line rxjs/no-unsafe-switchmap - ) - ); - - /** - * ** Constructor. - */ - constructor( - actions$: Actions, - componentService: ComponentService, - private readonly dataJobsApiService: DataJobsApiService - ) { - super(actions$, componentService, DataJobsEffects.CLASS_NAME); - - this.registerEffectsErrorCodes(); - } - - /** - * @inheritDoc - * @protected - */ - protected registerEffectsErrorCodes(): void { - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE] = this.dataJobsApiService.errorCodes.getJob; - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS] = this.dataJobsApiService.errorCodes.getJobDetails; - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS] = this.dataJobsApiService.errorCodes.getJobExecutions; - - LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE] = this.dataJobsApiService.errorCodes.getJobs; - - UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_STATUS] = this.dataJobsApiService.errorCodes.updateDataJobStatus; - UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_DESCRIPTION] = this.dataJobsApiService.errorCodes.updateDataJob; - } - - private _loadDataJobs(componentModel: ComponentModel): Observable { - const componentState = componentModel.getComponentState(); - const task: DataJobsLoadTasks = TASK_LOAD_JOBS_STATE; - - return of(componentModel).pipe( - switchMap((model) => - this.dataJobsApiService - .getJobs(componentState.filter.criteria, componentState.search, componentState.page.page, componentState.page.size) - .pipe( - map((response) => - model - .clearTask() - .removeErrorCodePatterns(LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE].All) - .withData(JOBS_DATA_KEY, response.data) - .withTask(task) - .withStatusLoaded() - .getComponentState() - ), - map((state) => ComponentLoaded.of(state)), - catchError>((error: unknown) => - this._getLatestModel(model).pipe( - map((newModel) => - ComponentFailed.of( - newModel - .withData(JOBS_DATA_KEY, { - content: [], - totalItems: 0, - totalPages: 0 - } as DataJobPage) - .withError( - processServiceRequestError( - this.objectUUID, - LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE], - ErrorUtil.extractError(error as Error) - ) - ) - .withTask(task) - .withStatusFailed() - .getComponentState() - ) - ) - ) + .withData(JOBS_DATA_KEY, response.data) + .withTask(task) + .withStatusLoaded() + .getComponentState(), + ), + map((state) => + ComponentLoaded.of(state), + ), + catchError>( + (error: unknown) => + this._getLatestModel(model).pipe( + map((newModel) => + ComponentFailed.of( + newModel + .withData(JOBS_DATA_KEY, { + content: [], + totalItems: 0, + totalPages: 0, + } as DataJobPage) + .withError( + processServiceRequestError( + this.objectUUID, + LOAD_JOBS_ERROR_CODES[TASK_LOAD_JOBS_STATE], + ErrorUtil.extractError(error as Error), + ), ) - ) - ) + .withTask(task) + .withStatusFailed() + .getComponentState(), + ), + ), + ), + ), + ), + ), + ); + } + + private _executeJobTask( + model: ComponentModel, + task: DataJobLoadTasks, + ): Observable { + switch (task) { + case TASK_LOAD_JOB_STATE: + return this._fetchJobData( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.dataJobsApiService.getJob.bind(this.dataJobsApiService), + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE], + model, + TASK_LOAD_JOB_STATE, + JOB_STATE_DATA_KEY, ); - } - - private _executeJobTask(model: ComponentModel, task: DataJobLoadTasks): Observable { - switch (task) { - case TASK_LOAD_JOB_STATE: - return this._fetchJobData( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.dataJobsApiService.getJob.bind(this.dataJobsApiService), - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_STATE], - model, - TASK_LOAD_JOB_STATE, - JOB_STATE_DATA_KEY - ); - case TASK_LOAD_JOB_DETAILS: - return this._fetchJobData( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.dataJobsApiService.getJobDetails.bind(this.dataJobsApiService), - LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS], - model, - TASK_LOAD_JOB_DETAILS, - JOB_DETAILS_DATA_KEY - ); - case TASK_LOAD_JOB_EXECUTIONS: - return this._loadDataJobExecutionsGraphQL(model); - default: - return throwError(() => new Error('Unknown action task for Data Pipelines.')).pipe(this._handleError(model, null, task)); - } - } - - private _fetchJobData( - executor: (param1: string, param2: string) => Observable, - executorErrorCodes: Readonly>, - componentModel: ComponentModel, - task: DataJobLoadTasks | string, - dataKey: string - ): Observable { - return of(componentModel).pipe( - switchMap((model) => - executor( - componentModel.getComponentState().requestParams.get(TEAM_NAME_REQ_PARAM) as string, - componentModel.getComponentState().requestParams.get(JOB_NAME_REQ_PARAM) as string - ).pipe( - switchMap((data) => { - let obsoleteStatus: StatusType; - - return this._getLatestModel(model).pipe( - tap((newModel) => (obsoleteStatus = newModel.status)), - map((newModel) => - newModel - .removeErrorCodePatterns(executorErrorCodes.All) - .withTask(task) - .withData(dataKey, data) - .withStatusLoaded() - .getComponentState() - ), - map((state) => (obsoleteStatus === LOADED ? ComponentUpdate.of(state) : ComponentLoaded.of(state))) - ); - }), - this._handleError(model, executorErrorCodes, task) - ) - ) + case TASK_LOAD_JOB_DETAILS: + return this._fetchJobData( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.dataJobsApiService.getJobDetails.bind(this.dataJobsApiService), + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_DETAILS], + model, + TASK_LOAD_JOB_DETAILS, + JOB_DETAILS_DATA_KEY, ); + case TASK_LOAD_JOB_EXECUTIONS: + return this._loadDataJobExecutionsGraphQL(model); + default: + return throwError( + () => new Error("Unknown action task for Data Pipelines."), + ).pipe(this._handleError(model, null, task)); } - - private _updateJob(model: ComponentModel, taskIdentifier: string): Observable { - const task = extractTaskFromIdentifier(taskIdentifier); - const requestParams = model.getComponentState().requestParams; - - if (task === TASK_UPDATE_JOB_DESCRIPTION) { - const jobDetails: DataJobDetails = requestParams.get(JOB_DETAILS_REQ_PARAM); - - return this.dataJobsApiService - .updateDataJob( - requestParams.get(TEAM_NAME_REQ_PARAM) as string, - requestParams.get(JOB_NAME_REQ_PARAM) as string, - jobDetails - ) - .pipe( - map(() => - ComponentLoaded.of( - model - .removeErrorCodePatterns(UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_DESCRIPTION].All) - .withTask(taskIdentifier) - .withData(JOB_DETAILS_DATA_KEY, jobDetails) - .withStatusLoaded() - .getComponentState() - ) - ), - this._handleError(model, UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_DESCRIPTION], taskIdentifier) - ); - } - - if (task === TASK_UPDATE_JOB_STATUS) { - const jobState: DataJob = model.getComponentState().requestParams.get(JOB_STATE_REQ_PARAM); - - return this.dataJobsApiService - .updateDataJobStatus( - requestParams.get(TEAM_NAME_REQ_PARAM) as string, - requestParams.get(JOB_NAME_REQ_PARAM) as string, - requestParams.get(JOB_DEPLOYMENT_ID_REQ_PARAM) as string, - requestParams.get(JOB_STATUS_REQ_PARAM) as boolean + } + + private _fetchJobData( + executor: (param1: string, param2: string) => Observable, + executorErrorCodes: Readonly>, + componentModel: ComponentModel, + task: DataJobLoadTasks | string, + dataKey: string, + ): Observable { + return of(componentModel).pipe( + switchMap((model) => + executor( + componentModel + .getComponentState() + .requestParams.get(TEAM_NAME_REQ_PARAM) as string, + componentModel + .getComponentState() + .requestParams.get(JOB_NAME_REQ_PARAM) as string, + ).pipe( + switchMap((data) => { + let obsoleteStatus: StatusType; + + return this._getLatestModel(model).pipe( + tap((newModel) => (obsoleteStatus = newModel.status)), + map((newModel) => + newModel + .removeErrorCodePatterns(executorErrorCodes.All) + .withTask(task) + .withData(dataKey, data) + .withStatusLoaded() + .getComponentState(), + ), + map((state) => + obsoleteStatus === LOADED + ? ComponentUpdate.of(state) + : ComponentLoaded.of(state), + ), + ); + }), + this._handleError(model, executorErrorCodes, task), + ), + ), + ); + } + + private _updateJob( + model: ComponentModel, + taskIdentifier: string, + ): Observable { + const task = extractTaskFromIdentifier(taskIdentifier); + const requestParams = model.getComponentState().requestParams; + + if (task === TASK_UPDATE_JOB_DESCRIPTION) { + const jobDetails: DataJobDetails = requestParams.get( + JOB_DETAILS_REQ_PARAM, + ); + + return this.dataJobsApiService + .updateDataJob( + requestParams.get(TEAM_NAME_REQ_PARAM) as string, + requestParams.get(JOB_NAME_REQ_PARAM) as string, + jobDetails, + ) + .pipe( + map(() => + ComponentLoaded.of( + model + .removeErrorCodePatterns( + UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_DESCRIPTION] + .All, ) - .pipe( - map(() => - ComponentLoaded.of( - model - .removeErrorCodePatterns(UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_STATUS].All) - .withTask(taskIdentifier) - .withData(JOB_STATE_DATA_KEY, jobState) - .withStatusLoaded() - .getComponentState() - ) - ), - this._handleError(model, UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_STATUS], taskIdentifier) - ); - } - - const error = new Error('Unsupported action task for Data Pipelines, update Data Job.'); - - console.error(error); - - return of( - ComponentFailed.of( - model - .withTask(taskIdentifier) - .withError({ - objectUUID: this.objectUUID, - code: generateErrorCode( - DataJobsEffects.CLASS_NAME, - DataJobsEffects.PUBLIC_NAME, - '_updateJob', - 'UnsupportedActionTask' - ), - error - }) - .withStatusFailed() - .getComponentState() - ) + .withTask(taskIdentifier) + .withData(JOB_DETAILS_DATA_KEY, jobDetails) + .withStatusLoaded() + .getComponentState(), + ), + ), + this._handleError( + model, + UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_DESCRIPTION], + taskIdentifier, + ), ); } - private _loadDataJobExecutionsGraphQL(componentModel: ComponentModel): Observable { - const componentState = componentModel.getComponentState(); - const requestParams = componentState.requestParams; - - return of(componentModel).pipe( - switchMap((model) => - this.dataJobsApiService - .getJobExecutions( - requestParams.get(TEAM_NAME_REQ_PARAM) as string, - requestParams.get(JOB_NAME_REQ_PARAM) as string, - true, - requestParams.get(FILTER_REQ_PARAM) as DataJobExecutionFilter, - requestParams.get(ORDER_REQ_PARAM) as DataJobExecutionOrder - ) - .pipe( - switchMap((response) => { - let obsoleteStatus: StatusType; - - return this._getLatestModel(model).pipe( - tap((newModel) => (obsoleteStatus = newModel.status)), - map((newModel) => - newModel - .removeErrorCodePatterns(LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS].All) - .withTask(TASK_LOAD_JOB_EXECUTIONS) - .withData(JOB_EXECUTIONS_DATA_KEY, response.content) - .withStatusLoaded() - .getComponentState() - ), - map((state) => (obsoleteStatus === LOADED ? ComponentUpdate.of(state) : ComponentLoaded.of(state))) - ); - }), - this._handleError(model, LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS], TASK_LOAD_JOB_EXECUTIONS) - ) - ) + if (task === TASK_UPDATE_JOB_STATUS) { + const jobState: DataJob = model + .getComponentState() + .requestParams.get(JOB_STATE_REQ_PARAM); + + return this.dataJobsApiService + .updateDataJobStatus( + requestParams.get(TEAM_NAME_REQ_PARAM) as string, + requestParams.get(JOB_NAME_REQ_PARAM) as string, + requestParams.get(JOB_DEPLOYMENT_ID_REQ_PARAM) as string, + requestParams.get(JOB_STATUS_REQ_PARAM) as boolean, + ) + .pipe( + map(() => + ComponentLoaded.of( + model + .removeErrorCodePatterns( + UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_STATUS].All, + ) + .withTask(taskIdentifier) + .withData(JOB_STATE_DATA_KEY, jobState) + .withStatusLoaded() + .getComponentState(), + ), + ), + this._handleError( + model, + UPDATE_JOB_DETAILS_ERROR_CODES[TASK_UPDATE_JOB_STATUS], + taskIdentifier, + ), ); } - private _getLatestModel(componentModel: ComponentModel): Observable { - const componentState = componentModel.getComponentState(); - - return this.componentService.getModel(componentState.id, componentState.routePathSegments, ['*']).pipe(take(1)); - } + const error = new Error( + "Unsupported action task for Data Pipelines, update Data Job.", + ); - private _handleError( - obsoleteModel: ComponentModel, - executorErrorCodes: Readonly>, - task: DataJobLoadTasks | string - ) { - return catchError>((error: unknown) => { - return this._getLatestModel(obsoleteModel).pipe( + console.error(error); + + return of( + ComponentFailed.of( + model + .withTask(taskIdentifier) + .withError({ + objectUUID: this.objectUUID, + code: generateErrorCode( + DataJobsEffects.CLASS_NAME, + DataJobsEffects.PUBLIC_NAME, + "_updateJob", + "UnsupportedActionTask", + ), + error, + }) + .withStatusFailed() + .getComponentState(), + ), + ); + } + + private _loadDataJobExecutionsGraphQL( + componentModel: ComponentModel, + ): Observable { + const componentState = componentModel.getComponentState(); + const requestParams = componentState.requestParams; + + return of(componentModel).pipe( + switchMap((model) => + this.dataJobsApiService + .getJobExecutions( + requestParams.get(TEAM_NAME_REQ_PARAM) as string, + requestParams.get(JOB_NAME_REQ_PARAM) as string, + true, + requestParams.get(FILTER_REQ_PARAM) as DataJobExecutionFilter, + requestParams.get(ORDER_REQ_PARAM) as DataJobExecutionOrder, + ) + .pipe( + switchMap((response) => { + let obsoleteStatus: StatusType; + + return this._getLatestModel(model).pipe( + tap((newModel) => (obsoleteStatus = newModel.status)), map((newModel) => - newModel - .withTask(task) - .withError( - CollectionsUtil.isLiteralObject(executorErrorCodes) - ? processServiceRequestError(this.objectUUID, executorErrorCodes, ErrorUtil.extractError(error as Error)) - : { - objectUUID: this.objectUUID, - code: generateErrorCode( - DataJobsEffects.CLASS_NAME, - DataJobsEffects.PUBLIC_NAME, - '_handleError', - 'GenericError' - ), - error: error as Error - } - ) - .withStatusFailed() - .getComponentState() + newModel + .removeErrorCodePatterns( + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS].All, + ) + .withTask(TASK_LOAD_JOB_EXECUTIONS) + .withData(JOB_EXECUTIONS_DATA_KEY, response.content) + .withStatusLoaded() + .getComponentState(), ), - map((state) => ComponentFailed.of(state)) - ); - }); - } + map((state) => + obsoleteStatus === LOADED + ? ComponentUpdate.of(state) + : ComponentLoaded.of(state), + ), + ); + }), + this._handleError( + model, + LOAD_JOB_ERROR_CODES[TASK_LOAD_JOB_EXECUTIONS], + TASK_LOAD_JOB_EXECUTIONS, + ), + ), + ), + ); + } + + private _getLatestModel( + componentModel: ComponentModel, + ): Observable { + const componentState = componentModel.getComponentState(); + + return this.componentService + .getModel(componentState.id, componentState.routePathSegments, ["*"]) + .pipe(take(1)); + } + + private _handleError( + obsoleteModel: ComponentModel, + executorErrorCodes: Readonly>, + task: DataJobLoadTasks | string, + ) { + return catchError< + ComponentLoaded | ComponentUpdate, + Observable + >((error: unknown) => { + return this._getLatestModel(obsoleteModel).pipe( + map((newModel) => + newModel + .withTask(task) + .withError( + CollectionsUtil.isLiteralObject(executorErrorCodes) + ? processServiceRequestError( + this.objectUUID, + executorErrorCodes, + ErrorUtil.extractError(error as Error), + ) + : { + objectUUID: this.objectUUID, + code: generateErrorCode( + DataJobsEffects.CLASS_NAME, + DataJobsEffects.PUBLIC_NAME, + "_handleError", + "GenericError", + ), + error: error as Error, + }, + ) + .withStatusFailed() + .getComponentState(), + ), + map((state) => + ComponentFailed.of(state), + ), + ); + }); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/index.ts index b30e8ce31a..d9fe8a23ee 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/effects/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-jobs.effects'; +export * from "./data-jobs.effects"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/data-job.error-codes.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/data-job.error-codes.ts index 58083a392d..b5f7be4cfa 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/data-job.error-codes.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/data-job.error-codes.ts @@ -3,41 +3,49 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ServiceHttpErrorCodes } from '@versatiledatakit/shared'; +import { ServiceHttpErrorCodes } from "@versatiledatakit/shared"; import { - TASK_LOAD_JOB_DETAILS, - TASK_LOAD_JOB_EXECUTIONS, - TASK_LOAD_JOB_STATE, - TASK_LOAD_JOBS_STATE, - TASK_UPDATE_JOB_DESCRIPTION, - TASK_UPDATE_JOB_STATUS -} from '../tasks'; + TASK_LOAD_JOB_DETAILS, + TASK_LOAD_JOB_EXECUTIONS, + TASK_LOAD_JOB_STATE, + TASK_LOAD_JOBS_STATE, + TASK_UPDATE_JOB_DESCRIPTION, + TASK_UPDATE_JOB_STATUS, +} from "../tasks"; // load tasks error codes export const LOAD_JOB_ERROR_CODES: { - [TASK_LOAD_JOB_STATE]: Readonly>; - [TASK_LOAD_JOB_DETAILS]: Readonly>; - [TASK_LOAD_JOB_EXECUTIONS]: Readonly>; + [TASK_LOAD_JOB_STATE]: Readonly>; + [TASK_LOAD_JOB_DETAILS]: Readonly< + Record + >; + [TASK_LOAD_JOB_EXECUTIONS]: Readonly< + Record + >; } = { - [TASK_LOAD_JOB_STATE]: null, - [TASK_LOAD_JOB_DETAILS]: null, - [TASK_LOAD_JOB_EXECUTIONS]: null + [TASK_LOAD_JOB_STATE]: null, + [TASK_LOAD_JOB_DETAILS]: null, + [TASK_LOAD_JOB_EXECUTIONS]: null, }; export const LOAD_JOBS_ERROR_CODES: { - [TASK_LOAD_JOBS_STATE]: Readonly>; + [TASK_LOAD_JOBS_STATE]: Readonly>; } = { - [TASK_LOAD_JOBS_STATE]: null + [TASK_LOAD_JOBS_STATE]: null, }; // update tasks error codes export const UPDATE_JOB_DETAILS_ERROR_CODES: { - [TASK_UPDATE_JOB_STATUS]: Readonly>; - [TASK_UPDATE_JOB_DESCRIPTION]: Readonly>; + [TASK_UPDATE_JOB_STATUS]: Readonly< + Record + >; + [TASK_UPDATE_JOB_DESCRIPTION]: Readonly< + Record + >; } = { - [TASK_UPDATE_JOB_STATUS]: null, - [TASK_UPDATE_JOB_DESCRIPTION]: null + [TASK_UPDATE_JOB_STATUS]: null, + [TASK_UPDATE_JOB_DESCRIPTION]: null, }; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/index.ts index 787a602fdd..feac5e6d5d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/error-codes/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job.error-codes'; +export * from "./data-job.error-codes"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/data-job.tasks.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/data-job.tasks.ts index d85fa0db50..31090c9b3c 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/data-job.tasks.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/data-job.tasks.ts @@ -5,22 +5,27 @@ // load tasks -export const TASK_LOAD_JOB_STATE = 'load_job_state'; +export const TASK_LOAD_JOB_STATE = "load_job_state"; -export const TASK_LOAD_JOB_DETAILS = 'load_job_details'; +export const TASK_LOAD_JOB_DETAILS = "load_job_details"; -export const TASK_LOAD_JOB_EXECUTIONS = 'load_job_executions'; +export const TASK_LOAD_JOB_EXECUTIONS = "load_job_executions"; -export type DataJobLoadTasks = typeof TASK_LOAD_JOB_STATE | typeof TASK_LOAD_JOB_DETAILS | typeof TASK_LOAD_JOB_EXECUTIONS; +export type DataJobLoadTasks = + | typeof TASK_LOAD_JOB_STATE + | typeof TASK_LOAD_JOB_DETAILS + | typeof TASK_LOAD_JOB_EXECUTIONS; -export const TASK_LOAD_JOBS_STATE = 'load_jobs_state'; +export const TASK_LOAD_JOBS_STATE = "load_jobs_state"; export type DataJobsLoadTasks = typeof TASK_LOAD_JOBS_STATE; // update tasks -export const TASK_UPDATE_JOB_DESCRIPTION = 'update_job_description'; +export const TASK_UPDATE_JOB_DESCRIPTION = "update_job_description"; -export const TASK_UPDATE_JOB_STATUS = 'update_job_status'; +export const TASK_UPDATE_JOB_STATUS = "update_job_status"; -export type DataJobUpdateTasks = typeof TASK_UPDATE_JOB_DESCRIPTION | typeof TASK_UPDATE_JOB_STATUS; +export type DataJobUpdateTasks = + | typeof TASK_UPDATE_JOB_DESCRIPTION + | typeof TASK_UPDATE_JOB_STATUS; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/index.ts index abb5fe9ab2..caf6989587 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/state/tasks/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './data-job.tasks'; +export * from "./data-job.tasks"; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/vdk-data-pipelines.module.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/vdk-data-pipelines.module.ts index 2a56c26735..f507052d71 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/vdk-data-pipelines.module.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/vdk-data-pipelines.module.ts @@ -3,176 +3,188 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule, Routes } from '@angular/router'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule, Routes } from "@angular/router"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { TruncateModule } from '@yellowspot/ng-truncate'; +import { TruncateModule } from "@yellowspot/ng-truncate"; -import { NgxChartsModule } from '@swimlane/ngx-charts'; +import { NgxChartsModule } from "@swimlane/ngx-charts"; -import { TimeagoModule } from 'ngx-timeago'; -import { LottieModule } from 'ngx-lottie'; +import { TimeagoModule } from "ngx-timeago"; +import { LottieModule } from "ngx-lottie"; -import { DpDatePickerModule } from 'ng2-date-picker'; +import { DpDatePickerModule } from "ng2-date-picker"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { VdkSharedComponentsModule, VdkSharedFeaturesModule, VdkSharedNgRxModule } from '@versatiledatakit/shared'; +import { + VdkSharedComponentsModule, + VdkSharedFeaturesModule, + VdkSharedNgRxModule, +} from "@versatiledatakit/shared"; -import { AttributesDirective } from './shared/directives'; +import { AttributesDirective } from "./shared/directives"; import { - ContactsPresentPipe, - ExecutionSuccessRatePipe, - ExtractContactsPipe, - ExtractJobStatusPipe, - FormatDeltaPipe, - FormatSchedulePipe, - ParseEpochPipe, - ParseNextRunPipe -} from './shared/pipes'; + ContactsPresentPipe, + ExecutionSuccessRatePipe, + ExtractContactsPipe, + ExtractJobStatusPipe, + FormatDeltaPipe, + FormatSchedulePipe, + ParseEpochPipe, + ParseNextRunPipe, +} from "./shared/pipes"; import { - ColumnFilterComponent, - ConfirmationDialogModalComponent, - DeleteModalComponent, - EmptyStateComponent, - ExecutionsTimelineComponent, - GridActionComponent, - QuickFiltersComponent, - StatusCellComponent, - StatusPanelComponent, - WidgetValueComponent -} from './shared/components'; + ColumnFilterComponent, + ConfirmationDialogModalComponent, + DeleteModalComponent, + EmptyStateComponent, + ExecutionsTimelineComponent, + GridActionComponent, + QuickFiltersComponent, + StatusCellComponent, + StatusPanelComponent, + WidgetValueComponent, +} from "./shared/components"; -import { DataJobsApiService, DataJobsBaseApiService, DataJobsPublicApiService, DataJobsService, DataJobsServiceImpl } from './services'; +import { + DataJobsApiService, + DataJobsBaseApiService, + DataJobsPublicApiService, + DataJobsService, + DataJobsServiceImpl, +} from "./services"; -import { DATA_PIPELINES_CONFIGS, DataPipelinesConfig } from './model'; +import { DATA_PIPELINES_CONFIGS, DataPipelinesConfig } from "./model"; -import { DataJobsEffects } from './state/effects'; +import { DataJobsEffects } from "./state/effects"; -import { FormatDurationPipe } from './shared/pipes/format-duration.pipe'; +import { FormatDurationPipe } from "./shared/pipes/format-duration.pipe"; -import { DataJobsExplorePageComponent } from './components/data-jobs-explore'; -import { DataJobsExploreGridComponent } from './components/data-jobs-explore/components/grid'; +import { DataJobsExplorePageComponent } from "./components/data-jobs-explore"; +import { DataJobsExploreGridComponent } from "./components/data-jobs-explore/components/grid"; -import { DataJobsManagePageComponent } from './components/data-jobs-manage'; -import { DataJobsManageGridComponent } from './components/data-jobs-manage/components/grid'; +import { DataJobsManagePageComponent } from "./components/data-jobs-manage"; +import { DataJobsManageGridComponent } from "./components/data-jobs-manage/components/grid"; -import { DataJobPageComponent } from './components/data-job'; -import { DataJobDetailsPageComponent } from './components/data-job/pages/details'; +import { DataJobPageComponent } from "./components/data-job"; +import { DataJobDetailsPageComponent } from "./components/data-job/pages/details"; import { - DataJobDeploymentDetailsModalComponent, - DataJobExecutionsGridComponent, + DataJobDeploymentDetailsModalComponent, + DataJobExecutionsGridComponent, + DataJobExecutionsPageComponent, + DataJobExecutionStatusComponent, + DataJobExecutionStatusFilterComponent, + DataJobExecutionTypeComponent, + DataJobExecutionTypeFilterComponent, + ExecutionDurationChartComponent, + ExecutionStatusChartComponent, + TimePeriodFilterComponent, +} from "./components/data-job/pages/executions"; + +import { + DataJobsExecutionsWidgetComponent, + DataJobsFailedWidgetComponent, + DataJobsHealthPanelComponent, + DataJobsWidgetOneComponent, + WidgetExecutionStatusGaugeComponent, +} from "./components/widgets"; + +const routes: Routes = []; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(routes), + FormsModule, + ReactiveFormsModule, + LottieModule, + TruncateModule, + TimeagoModule.forRoot(), + ClarityModule, + DpDatePickerModule, + NgxChartsModule, + VdkSharedComponentsModule.forChild(), + VdkSharedFeaturesModule.forChild(), + VdkSharedNgRxModule.forFeatureEffects([DataJobsEffects]), + ], + declarations: [ + AttributesDirective, + FormatDeltaPipe, + FormatSchedulePipe, + ParseNextRunPipe, + ContactsPresentPipe, + ExecutionSuccessRatePipe, + ExtractJobStatusPipe, + ExtractContactsPipe, + ParseEpochPipe, + DataJobsExplorePageComponent, + DataJobsExploreGridComponent, + DataJobsManagePageComponent, + DataJobsManageGridComponent, + DataJobPageComponent, + DataJobDetailsPageComponent, DataJobExecutionsPageComponent, - DataJobExecutionStatusComponent, - DataJobExecutionStatusFilterComponent, DataJobExecutionTypeComponent, + DataJobExecutionStatusFilterComponent, + DataJobDeploymentDetailsModalComponent, + DataJobExecutionsGridComponent, DataJobExecutionTypeFilterComponent, - ExecutionDurationChartComponent, + TimePeriodFilterComponent, ExecutionStatusChartComponent, - TimePeriodFilterComponent -} from './components/data-job/pages/executions'; - -import { + ExecutionDurationChartComponent, + DataJobExecutionStatusComponent, + DeleteModalComponent, + ConfirmationDialogModalComponent, + GridActionComponent, + StatusCellComponent, + StatusPanelComponent, + ExecutionsTimelineComponent, + // Widgets + DataJobsWidgetOneComponent, + WidgetValueComponent, + ColumnFilterComponent, + FormatDurationPipe, + QuickFiltersComponent, DataJobsExecutionsWidgetComponent, DataJobsFailedWidgetComponent, + WidgetExecutionStatusGaugeComponent, DataJobsHealthPanelComponent, + EmptyStateComponent, + ], + exports: [ + DataJobsExplorePageComponent, + DataJobsExploreGridComponent, + DataJobsManagePageComponent, + DataJobsManageGridComponent, + DataJobPageComponent, + DataJobDetailsPageComponent, + DataJobExecutionsPageComponent, DataJobsWidgetOneComponent, - WidgetExecutionStatusGaugeComponent -} from './components/widgets'; - -const routes: Routes = []; - -@NgModule({ - imports: [ - CommonModule, - RouterModule.forChild(routes), - FormsModule, - ReactiveFormsModule, - LottieModule, - TruncateModule, - TimeagoModule.forRoot(), - ClarityModule, - DpDatePickerModule, - NgxChartsModule, - VdkSharedComponentsModule.forChild(), - VdkSharedFeaturesModule.forChild(), - VdkSharedNgRxModule.forFeatureEffects([DataJobsEffects]) - ], - declarations: [ - AttributesDirective, - FormatDeltaPipe, - FormatSchedulePipe, - ParseNextRunPipe, - ContactsPresentPipe, - ExecutionSuccessRatePipe, - ExtractJobStatusPipe, - ExtractContactsPipe, - ParseEpochPipe, - DataJobsExplorePageComponent, - DataJobsExploreGridComponent, - DataJobsManagePageComponent, - DataJobsManageGridComponent, - DataJobPageComponent, - DataJobDetailsPageComponent, - DataJobExecutionsPageComponent, - DataJobExecutionTypeComponent, - DataJobExecutionStatusFilterComponent, - DataJobDeploymentDetailsModalComponent, - DataJobExecutionsGridComponent, - DataJobExecutionTypeFilterComponent, - TimePeriodFilterComponent, - ExecutionStatusChartComponent, - ExecutionDurationChartComponent, - DataJobExecutionStatusComponent, - DeleteModalComponent, - ConfirmationDialogModalComponent, - GridActionComponent, - StatusCellComponent, - StatusPanelComponent, - ExecutionsTimelineComponent, - // Widgets - DataJobsWidgetOneComponent, - WidgetValueComponent, - ColumnFilterComponent, - FormatDurationPipe, - QuickFiltersComponent, - DataJobsExecutionsWidgetComponent, - DataJobsFailedWidgetComponent, - WidgetExecutionStatusGaugeComponent, - DataJobsHealthPanelComponent, - EmptyStateComponent - ], - exports: [ - DataJobsExplorePageComponent, - DataJobsExploreGridComponent, - DataJobsManagePageComponent, - DataJobsManageGridComponent, - DataJobPageComponent, - DataJobDetailsPageComponent, - DataJobExecutionsPageComponent, - DataJobsWidgetOneComponent, - DataJobsExecutionsWidgetComponent, - DataJobsFailedWidgetComponent, - WidgetExecutionStatusGaugeComponent, - DataJobsHealthPanelComponent - ] + DataJobsExecutionsWidgetComponent, + DataJobsFailedWidgetComponent, + WidgetExecutionStatusGaugeComponent, + DataJobsHealthPanelComponent, + ], }) export class VdkDataPipelinesModule { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static forRoot(config: DataPipelinesConfig = {} as any): ModuleWithProviders { - return { - ngModule: VdkDataPipelinesModule, - providers: [ - DataJobsBaseApiService, - DataJobsPublicApiService, - DataJobsApiService, - { provide: DataJobsService, useClass: DataJobsServiceImpl }, - { provide: DATA_PIPELINES_CONFIGS, useValue: config } - ] - }; - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static forRoot( + config: DataPipelinesConfig = {} as any, + ): ModuleWithProviders { + return { + ngModule: VdkDataPipelinesModule, + providers: [ + DataJobsBaseApiService, + DataJobsPublicApiService, + DataJobsApiService, + { provide: DataJobsService, useClass: DataJobsServiceImpl }, + { provide: DATA_PIPELINES_CONFIGS, useValue: config }, + ], + }; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/public-api.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/public-api.ts index 2a97140c68..3d609b8ebd 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/public-api.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/public-api.ts @@ -7,15 +7,15 @@ * Public API Surface of data-pipelines */ -export * from './lib/vdk-data-pipelines.module'; +export * from "./lib/vdk-data-pipelines.module"; // Components / Pages -export * from './lib/components/public-api'; +export * from "./lib/components/public-api"; // Services -export * from './lib/services/public-api'; +export * from "./lib/services/public-api"; // Models -export * from './lib/model/public-api'; +export * from "./lib/model/public-api"; // Shared diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/test.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/test.ts index 705c769e34..3689d5a9c0 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/test.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/test.ts @@ -5,27 +5,34 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js'; -import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; +import "zone.js"; +import "zone.js/testing"; +import { getTestBed } from "@angular/core/testing"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; declare const require: { - context( - path: string, - deep?: boolean, - filter?: RegExp - ): { - keys(): string[]; - (id: string): T; - }; + context( + path: string, + deep?: boolean, + filter?: RegExp, + ): { + keys(): string[]; + (id: string): T; + }; }; // First, initialize the Angular testing environment. -getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { - teardown: { destroyAfterEach: false } -}); +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false }, + }, +); // Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); +const context = require.context("./", true, /\.spec\.ts$/); // And load the modules. context.keys().forEach(context); diff --git a/projects/frontend/data-pipelines/gui/projects/ui/karma.conf.js b/projects/frontend/data-pipelines/gui/projects/ui/karma.conf.js index 48068ab2f3..bd03bdf740 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/karma.conf.js +++ b/projects/frontend/data-pipelines/gui/projects/ui/karma.conf.js @@ -7,54 +7,67 @@ // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-junit-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma')], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser + config.set({ + basePath: "", + frameworks: ["jasmine", "@angular-devkit/build-angular"], + plugins: [ + require("karma-jasmine"), + require("karma-chrome-launcher"), + require("karma-jasmine-html-reporter"), + require("karma-junit-reporter"), + require("karma-coverage"), + require("@angular-devkit/build-angular/plugins/karma"), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + coverageReporter: { + dir: require("path").join( + __dirname, + "../../reports/coverage/data-pipelines-ui", + ), + reporters: [ + //Code coverage - output only in HTML file + { type: "html" }, + { type: "text-summary" }, + { type: "lcovonly" }, + ], + check: { + global: { + lines: 20, }, - coverageReporter: { - dir: require('path').join(__dirname, '../../reports/coverage/data-pipelines-ui'), - reporters: [ - //Code coverage - output only in HTML file - { type: 'html' }, - { type: 'text-summary' }, - { type: 'lcovonly' } - ], - check: { - global: { - lines: 20 - } - } - }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - junitReporter: { - outputDir: require('path').join(__dirname, '../../reports/test-results/data-pipelines-ui'), - outputFile: 'unit-tests.xml', - useBrowserName: false - }, - reporters: ['progress', 'junit', 'coverage'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['ChromeHeadless'], - customLaunchers: { - ChromeHeadless_No_Sandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - }, - singleRun: false, - restartOnFileChange: true - }); + }, + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + junitReporter: { + outputDir: require("path").join( + __dirname, + "../../reports/test-results/data-pipelines-ui", + ), + outputFile: "unit-tests.xml", + useBrowserName: false, + }, + reporters: ["progress", "junit", "coverage"], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ["ChromeHeadless"], + customLaunchers: { + ChromeHeadless_No_Sandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + singleRun: false, + restartOnFileChange: true, + }); }; diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.model.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.model.ts index 67de4ff40f..cf0fa0d5ce 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.model.ts @@ -3,30 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AuthConfig, OAuthResourceServerConfig } from 'angular-oauth2-oidc'; +import { AuthConfig, OAuthResourceServerConfig } from "angular-oauth2-oidc"; export interface AppConfig { - auth: Auth; - ignoreRoutes?: string[]; - ignoreComponents?: string[]; + auth: Auth; + ignoreRoutes?: string[]; + ignoreComponents?: string[]; } export interface Auth { - skipAuth?: false; + skipAuth?: false; - // Used for producing the AuthConfig.customQueryParams mapping for `orgLink` and `targetUri` - consoleCloudUrl?: string; - orgLinkRoot?: string; - // $window.location.origin is replaced with the corresponding value dynamically upon loading, - // see AppConfigService - authConfig?: AuthConfig; - resourceServer?: OAuthResourceServerConfig; - // Used for token auto-refresh capability, in case a token is about to expire. - refreshTokenConfig?: RefreshTokenConfig; + // Used for producing the AuthConfig.customQueryParams mapping for `orgLink` and `targetUri` + consoleCloudUrl?: string; + orgLinkRoot?: string; + // $window.location.origin is replaced with the corresponding value dynamically upon loading, + // see AppConfigService + authConfig?: AuthConfig; + resourceServer?: OAuthResourceServerConfig; + // Used for token auto-refresh capability, in case a token is about to expire. + refreshTokenConfig?: RefreshTokenConfig; } export interface RefreshTokenConfig { - start?: number; - remainingTime?: number; - checkInterval?: number; + start?: number; + remainingTime?: number; + checkInterval?: number; } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.spec.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.spec.ts index a1024f6a1a..18e0ecf1d6 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.spec.ts @@ -3,22 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AppConfigService } from './app-config.service'; -import { HttpBackend } from '@angular/common/http'; +import { TestBed } from "@angular/core/testing"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { AppConfigService } from "./app-config.service"; +import { HttpBackend } from "@angular/common/http"; -describe('AppConfigService', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [{ provide: AppConfigService, useClass: AppConfigService, deps: [HttpBackend] }] - }); +describe("AppConfigService", () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: AppConfigService, + useClass: AppConfigService, + deps: [HttpBackend], + }, + ], }); + }); - it('can load instance', () => { - const service = TestBed.inject(AppConfigService); - service.loadAppConfig(); - expect(service).toBeTruthy(); - }); + it("can load instance", () => { + const service = TestBed.inject(AppConfigService); + service.loadAppConfig(); + expect(service).toBeTruthy(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.ts index 01389c3600..4d9dbaf900 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app-config.service.ts @@ -3,66 +3,72 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; -import { HttpBackend, HttpClient } from '@angular/common/http'; -import { AuthConfig } from 'angular-oauth2-oidc'; -import { AppConfig, RefreshTokenConfig } from './app-config.model'; -import { firstValueFrom } from 'rxjs'; -import { Router } from '@angular/router'; -import { routes } from './app.routing'; +import { Injectable } from "@angular/core"; +import { HttpBackend, HttpClient } from "@angular/common/http"; +import { AuthConfig } from "angular-oauth2-oidc"; +import { AppConfig, RefreshTokenConfig } from "./app-config.model"; +import { firstValueFrom } from "rxjs"; +import { Router } from "@angular/router"; +import { routes } from "./app.routing"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) export class AppConfigService { - private httpClient: HttpClient; - private appConfig: AppConfig; + private httpClient: HttpClient; + private appConfig: AppConfig; - constructor( - private httpBackend: HttpBackend, - private router: Router - ) { - this.httpClient = new HttpClient(httpBackend); - } + constructor( + private httpBackend: HttpBackend, + private router: Router, + ) { + this.httpClient = new HttpClient(httpBackend); + } - loadAppConfig(): Promise { - return firstValueFrom(this.httpClient.get('/assets/data/appConfig.json')).then((data) => { - this.appConfig = data; - try { - if (data.ignoreRoutes) { - const localRoutes = routes.filter((route: { path: string }) => { - return !data.ignoreRoutes.includes(route.path); - }); - this.router.resetConfig(localRoutes); - } - } catch (e) { - console.error('Failed to reset Router config'); - throw e; - } - }); - } + loadAppConfig(): Promise { + return firstValueFrom( + this.httpClient.get("/assets/data/appConfig.json"), + ).then((data) => { + this.appConfig = data; + try { + if (data.ignoreRoutes) { + const localRoutes = routes.filter((route: { path: string }) => { + return !data.ignoreRoutes.includes(route.path); + }); + this.router.resetConfig(localRoutes); + } + } catch (e) { + console.error("Failed to reset Router config"); + throw e; + } + }); + } - getConfig(): AppConfig { - return this.appConfig; - } + getConfig(): AppConfig { + return this.appConfig; + } - getSkipAuth(): boolean { - return this.appConfig.auth.skipAuth; - } + getSkipAuth(): boolean { + return this.appConfig.auth.skipAuth; + } - getAuthCodeFlowConfig(): AuthConfig { - if (this.getSkipAuth()) return new AuthConfig(); - const replaceWindowLocationOrigin = (str: string): string => { - return str?.replace('$window.location.origin', window.location.origin); - }; + getAuthCodeFlowConfig(): AuthConfig { + if (this.getSkipAuth()) return new AuthConfig(); + const replaceWindowLocationOrigin = (str: string): string => { + return str?.replace("$window.location.origin", window.location.origin); + }; - const authCodeFlowConfig: AuthConfig = this.getConfig()?.auth.authConfig; - authCodeFlowConfig.redirectUri = replaceWindowLocationOrigin(authCodeFlowConfig?.redirectUri); - authCodeFlowConfig.silentRefreshRedirectUri = replaceWindowLocationOrigin(authCodeFlowConfig?.silentRefreshRedirectUri); - return authCodeFlowConfig; - } + const authCodeFlowConfig: AuthConfig = this.getConfig()?.auth.authConfig; + authCodeFlowConfig.redirectUri = replaceWindowLocationOrigin( + authCodeFlowConfig?.redirectUri, + ); + authCodeFlowConfig.silentRefreshRedirectUri = replaceWindowLocationOrigin( + authCodeFlowConfig?.silentRefreshRedirectUri, + ); + return authCodeFlowConfig; + } - getRefreshTokenConfig(): RefreshTokenConfig { - if (this.getSkipAuth()) return null; - return this.getConfig()?.auth.refreshTokenConfig; - } + getRefreshTokenConfig(): RefreshTokenConfig { + if (this.getSkipAuth()) return null; + return this.getConfig()?.auth.refreshTokenConfig; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.html b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.html index f577a85a94..faaec0ba68 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.html +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.html @@ -4,120 +4,114 @@ --> - -
    -
    - - - Data Pipelines - + +
    + +
    + + + +
    + Logout
    -
    - - - -
    - Logout -
    -
    -
    -
    -
    + + +
    +
    -
    - - - - Get started - +
    + + + + Get started + - - - Explore - - - - - Data jobs - - - + + + Explore + + + + + Data jobs + + + - + - - - - Manage + + + + Manage - - - Data Jobs - - - + + + Data Jobs + + + -
    - -
    -
    - +
    + +
    +
    +
    -
    -
    -

    Loading Data

    - -
    +
    +
    +

    Loading Data

    +
    +
    diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.scss b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.scss index e8e4754b94..7228827864 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.scss +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.scss @@ -4,47 +4,47 @@ */ .small-text { - font-size: smaller; + font-size: smaller; } .vdk-main__spinner-container { - --vdk-spinner-background-color: white; - - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + --vdk-spinner-background-color: white; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--vdk-spinner-background-color); + z-index: 10001; + + .checking-user { + display: table; width: 100%; - height: 100%; - background-color: var(--vdk-spinner-background-color); - z-index: 10001; - - .checking-user { - display: table; - width: 100%; - margin-top: 200px; - - .loading-spinner { - text-align: center; - display: table-cell; - vertical-align: middle; - - .loading-title { - margin-top: 0; - margin-bottom: 20px; - } - } + margin-top: 200px; + + .loading-spinner { + text-align: center; + display: table-cell; + vertical-align: middle; + + .loading-title { + margin-top: 0; + margin-bottom: 20px; + } } + } } ::ng-deep .fade-to-dark.dark { - .vdk-main__spinner-container { - --vdk-spinner-background-color: #21333b; - } + .vdk-main__spinner-container { + --vdk-spinner-background-color: #21333b; + } } .nav-right { - float: right; - display: contents; + float: right; + display: contents; } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.spec.ts index 115579dfbc..a592c5cbb4 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.spec.ts @@ -3,130 +3,159 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA, ViewContainerRef } from '@angular/core'; +import { TestBed } from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA, ViewContainerRef } from "@angular/core"; -import { Subject } from 'rxjs'; +import { Subject } from "rxjs"; -import { OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; -import { TokenResponse } from 'angular-oauth2-oidc/types'; +import { OAuthService, UrlHelperService } from "angular-oauth2-oidc"; +import { TokenResponse } from "angular-oauth2-oidc/types"; import { - ConfirmationService, - DynamicComponentsService, - NavigationService, - RouterService, - UrlOpenerService -} from '@versatiledatakit/shared'; - -import { AppConfigService } from './app-config.service'; - -import { AppComponent } from './app.component'; -import { HttpBackend } from '@angular/common/http'; - -describe('AppComponent', () => { - let routerServiceStub: jasmine.SpyObj; - let navigationServiceStub: jasmine.SpyObj; - let viewContainerRefStub: jasmine.SpyObj; - let dynamicComponentsServiceStub: jasmine.SpyObj; - let confirmationServiceStub: jasmine.SpyObj; - let urlOpenerServiceStub: jasmine.SpyObj; - let appConfigServiceStub: jasmine.SpyObj; - const configureTestingModule = (providersAdditional: any[]) => { - const providersBase = [ - UrlHelperService, - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: RouterService, useValue: routerServiceStub }, - { provide: ViewContainerRef, useValue: viewContainerRefStub }, - { provide: DynamicComponentsService, useValue: dynamicComponentsServiceStub }, - { provide: ConfirmationService, useValue: confirmationServiceStub }, - { provide: UrlOpenerService, useValue: urlOpenerServiceStub }, - { provide: AppConfigService, useValue: appConfigServiceStub } - ]; - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [AppComponent], - imports: [], - providers: providersBase.concat(providersAdditional) - }); - }; - - beforeEach(() => { - routerServiceStub = jasmine.createSpyObj('routerService', ['getState']); - navigationServiceStub = jasmine.createSpyObj('navigationService', ['initialize']); - appConfigServiceStub = jasmine.createSpyObj('appConfigService', [ - 'getConfig', - 'getAuthCodeFlowConfig', - 'getSkipAuth' - ]); - viewContainerRefStub = jasmine.createSpyObj('viewContainerRefStub', ['createComponent']); - dynamicComponentsServiceStub = jasmine.createSpyObj('dynamicComponentsServiceStub', ['initialize']); - confirmationServiceStub = jasmine.createSpyObj('confirmationServiceStub', ['initialize']); - urlOpenerServiceStub = jasmine.createSpyObj('urlOpenerServiceStub', ['initialize']); - routerServiceStub.getState.and.returnValue(new Subject()); + ConfirmationService, + DynamicComponentsService, + NavigationService, + RouterService, + UrlOpenerService, +} from "@versatiledatakit/shared"; + +import { AppConfigService } from "./app-config.service"; + +import { AppComponent } from "./app.component"; +import { HttpBackend } from "@angular/common/http"; + +describe("AppComponent", () => { + let routerServiceStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let viewContainerRefStub: jasmine.SpyObj; + let dynamicComponentsServiceStub: jasmine.SpyObj; + let confirmationServiceStub: jasmine.SpyObj; + let urlOpenerServiceStub: jasmine.SpyObj; + let appConfigServiceStub: jasmine.SpyObj; + const configureTestingModule = (providersAdditional: any[]) => { + const providersBase = [ + UrlHelperService, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ViewContainerRef, useValue: viewContainerRefStub }, + { + provide: DynamicComponentsService, + useValue: dynamicComponentsServiceStub, + }, + { provide: ConfirmationService, useValue: confirmationServiceStub }, + { provide: UrlOpenerService, useValue: urlOpenerServiceStub }, + { provide: AppConfigService, useValue: appConfigServiceStub }, + ]; + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [AppComponent], + imports: [], + providers: providersBase.concat(providersAdditional), }); - - it('should create the app with auth enabled', () => { - appConfigServiceStub.getSkipAuth.and.returnValue(false); - const oAuthServiceStub = jasmine.createSpyObj('oAuthService', [ - 'configure', - 'loadDiscoveryDocumentAndLogin', - 'getAccessTokenExpiration', - 'refreshToken', - 'logOut', - 'getIdToken', - 'getIdentityClaims' - ]); - oAuthServiceStub.getIdentityClaims.and.returnValue({}); - oAuthServiceStub.loadDiscoveryDocumentAndLogin.and.returnValue(Promise.resolve(true)); - oAuthServiceStub.getAccessTokenExpiration.and.returnValue(0); - oAuthServiceStub.refreshToken.and.returnValue(Promise.resolve({} as TokenResponse)); - configureTestingModule([{ provide: OAuthService, useValue: oAuthServiceStub }]); - - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); + }; + + beforeEach(() => { + routerServiceStub = jasmine.createSpyObj("routerService", [ + "getState", + ]); + navigationServiceStub = jasmine.createSpyObj( + "navigationService", + ["initialize"], + ); + appConfigServiceStub = jasmine.createSpyObj( + "appConfigService", + ["getConfig", "getAuthCodeFlowConfig", "getSkipAuth"], + ); + viewContainerRefStub = jasmine.createSpyObj( + "viewContainerRefStub", + ["createComponent"], + ); + dynamicComponentsServiceStub = + jasmine.createSpyObj( + "dynamicComponentsServiceStub", + ["initialize"], + ); + confirmationServiceStub = jasmine.createSpyObj( + "confirmationServiceStub", + ["initialize"], + ); + urlOpenerServiceStub = jasmine.createSpyObj( + "urlOpenerServiceStub", + ["initialize"], + ); + routerServiceStub.getState.and.returnValue(new Subject()); + }); + + it("should create the app with auth enabled", () => { + appConfigServiceStub.getSkipAuth.and.returnValue(false); + const oAuthServiceStub = jasmine.createSpyObj( + "oAuthService", + [ + "configure", + "loadDiscoveryDocumentAndLogin", + "getAccessTokenExpiration", + "refreshToken", + "logOut", + "getIdToken", + "getIdentityClaims", + ], + ); + oAuthServiceStub.getIdentityClaims.and.returnValue({}); + oAuthServiceStub.loadDiscoveryDocumentAndLogin.and.returnValue( + Promise.resolve(true), + ); + oAuthServiceStub.getAccessTokenExpiration.and.returnValue(0); + oAuthServiceStub.refreshToken.and.returnValue( + Promise.resolve({} as TokenResponse), + ); + configureTestingModule([ + { provide: OAuthService, useValue: oAuthServiceStub }, + ]); + + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it("should create the app with auth skipped", () => { + appConfigServiceStub.getSkipAuth.and.returnValue(true); + configureTestingModule([]); + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it("should create the app with no components ignored", () => { + appConfigServiceStub.getConfig.and.returnValue({ + auth: {}, + ignoreComponents: [], + ignoreRoutes: [], }); - it('should create the app with auth skipped', () => { - appConfigServiceStub.getSkipAuth.and.returnValue(true); - configureTestingModule([]); - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); + // skip auth for convenience + appConfigServiceStub.getSkipAuth.and.returnValue(true); + + configureTestingModule([]); + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + expect(app.explorePageVisible).toBeTrue(); + }); + + it("should create the app with explore page ignored", () => { + appConfigServiceStub.getConfig.and.returnValue({ + auth: {}, + ignoreComponents: ["explorePage"], + ignoreRoutes: [], }); - it('should create the app with no components ignored', () => { - appConfigServiceStub.getConfig.and.returnValue({ - auth: {}, - ignoreComponents: [], - ignoreRoutes: [] - }); - - // skip auth for convenience - appConfigServiceStub.getSkipAuth.and.returnValue(true); - - configureTestingModule([]); - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - expect(app.explorePageVisible).toBeTrue(); - }); + // skip auth for convenience + appConfigServiceStub.getSkipAuth.and.returnValue(true); - it('should create the app with explore page ignored', () => { - appConfigServiceStub.getConfig.and.returnValue({ - auth: {}, - ignoreComponents: ['explorePage'], - ignoreRoutes: [] - }); - - // skip auth for convenience - appConfigServiceStub.getSkipAuth.and.returnValue(true); - - configureTestingModule([]); - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - expect(app.explorePageVisible).toBeFalse(); - }); + configureTestingModule([]); + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + expect(app.explorePageVisible).toBeFalse(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.ts index aa5cbb2f35..98fba35694 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.component.ts @@ -3,131 +3,151 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, OnInit, ViewContainerRef, Optional } from '@angular/core'; +import { Component, OnInit, ViewContainerRef, Optional } from "@angular/core"; -import { timer } from 'rxjs'; +import { timer } from "rxjs"; -import { OAuthService } from 'angular-oauth2-oidc'; +import { OAuthService } from "angular-oauth2-oidc"; -import { ConfirmationService, DynamicComponentsService, NavigationService, UrlOpenerService } from '@versatiledatakit/shared'; +import { + ConfirmationService, + DynamicComponentsService, + NavigationService, + UrlOpenerService, +} from "@versatiledatakit/shared"; -import { AppConfigService } from './app-config.service'; +import { AppConfigService } from "./app-config.service"; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.scss"], }) export class AppComponent implements OnInit { - title = 'core'; - collapsed = false; - - constructor( - private readonly appConfigService: AppConfigService, - @Optional() private readonly oauthService: OAuthService, - private readonly navigationService: NavigationService, - private readonly viewContainerRef: ViewContainerRef, - private readonly dynamicComponentsService: DynamicComponentsService, - private readonly confirmationService: ConfirmationService, - private readonly urlOpenerService: UrlOpenerService - ) { - if (!this.skipAuth) { - this.oauthService.configure(appConfigService.getAuthCodeFlowConfig()); - this.oauthService - .loadDiscoveryDocumentAndLogin() - .then(() => { - this.initTokenRefresh(); - }) - .catch(() => { - // No-op. - }); - } - } - - logout(): void { - if (this.skipAuth) return; - this.oauthService.logOut(); - } - - get skipAuth(): boolean { - return this.appConfigService.getSkipAuth(); - } - - get idToken(): string { - if (this.skipAuth) return null; - return this.oauthService.getIdToken(); - } - - get userName(): string { - if (this.skipAuth) return null; - return this.oauthService.getIdentityClaims() ? this.getIdentityClaim('username') : 'N/A'; - } - - get explorePageVisible(): boolean { - const ignoreComponents = this.appConfigService.getConfig().ignoreComponents; - return !(ignoreComponents && ignoreComponents.includes('explorePage')); - } - - /** - * @inheritDoc - */ - ngOnInit(): void { - this.navigationService.initialize(); - this.dynamicComponentsService.initialize(this.viewContainerRef); - this.confirmationService.initialize(); - this.urlOpenerService.initialize(); - } - - private getIdentityClaim(userNamePropName: string): string { - if (this.skipAuth) throw Error; - const identityClaims = this.oauthService.getIdentityClaims() as { - [key: string]: string; - }; - - return identityClaims[userNamePropName]; - } - - private initTokenRefresh() { - if (this.skipAuth) throw Error; - const refreshTokenConfig = this.appConfigService.getRefreshTokenConfig(); - timer(refreshTokenConfig.start, AppComponent.toMillis(refreshTokenConfig.checkInterval)).subscribe(() => { - const remainiTimeMillis = this.oauthService.getAccessTokenExpiration() - Date.now(); - if (remainiTimeMillis <= AppComponent.toMillis(refreshTokenConfig.remainingTime)) { - this.setCustomTokenAttributes(false, null); - this.oauthService.refreshToken().finally(() => { - // No-op. - }); - } + title = "core"; + collapsed = false; + + constructor( + private readonly appConfigService: AppConfigService, + @Optional() private readonly oauthService: OAuthService, + private readonly navigationService: NavigationService, + private readonly viewContainerRef: ViewContainerRef, + private readonly dynamicComponentsService: DynamicComponentsService, + private readonly confirmationService: ConfirmationService, + private readonly urlOpenerService: UrlOpenerService, + ) { + if (!this.skipAuth) { + this.oauthService.configure(appConfigService.getAuthCodeFlowConfig()); + this.oauthService + .loadDiscoveryDocumentAndLogin() + .then(() => { + this.initTokenRefresh(); + }) + .catch(() => { + // No-op. }); } - - private setCustomTokenAttributes(redirectToConsole: boolean, defaultOrg: { refLink: string }) { - if (this.skipAuth) throw Error; - const linkOrgQuery = this.getOrgLinkFromQueryParams(defaultOrg); - const consoleCloudUrl = this.appConfigService.getConfig().auth.consoleCloudUrl; - this.oauthService.customQueryParams = { - orgLink: linkOrgQuery, - targetUri: redirectToConsole ? consoleCloudUrl : window.location.href - }; - if (redirectToConsole) { - // Redirect to console cloud because we dont know the tenant url, but console does - this.oauthService.redirectUri = consoleCloudUrl; - } + } + + logout(): void { + if (this.skipAuth) return; + this.oauthService.logOut(); + } + + get skipAuth(): boolean { + return this.appConfigService.getSkipAuth(); + } + + get idToken(): string { + if (this.skipAuth) return null; + return this.oauthService.getIdToken(); + } + + get userName(): string { + if (this.skipAuth) return null; + return this.oauthService.getIdentityClaims() + ? this.getIdentityClaim("username") + : "N/A"; + } + + get explorePageVisible(): boolean { + const ignoreComponents = this.appConfigService.getConfig().ignoreComponents; + return !(ignoreComponents && ignoreComponents.includes("explorePage")); + } + + /** + * @inheritDoc + */ + ngOnInit(): void { + this.navigationService.initialize(); + this.dynamicComponentsService.initialize(this.viewContainerRef); + this.confirmationService.initialize(); + this.urlOpenerService.initialize(); + } + + private getIdentityClaim(userNamePropName: string): string { + if (this.skipAuth) throw Error; + const identityClaims = this.oauthService.getIdentityClaims() as { + [key: string]: string; + }; + + return identityClaims[userNamePropName]; + } + + private initTokenRefresh() { + if (this.skipAuth) throw Error; + const refreshTokenConfig = this.appConfigService.getRefreshTokenConfig(); + timer( + refreshTokenConfig.start, + AppComponent.toMillis(refreshTokenConfig.checkInterval), + ).subscribe(() => { + const remainiTimeMillis = + this.oauthService.getAccessTokenExpiration() - Date.now(); + if ( + remainiTimeMillis <= + AppComponent.toMillis(refreshTokenConfig.remainingTime) + ) { + this.setCustomTokenAttributes(false, null); + this.oauthService.refreshToken().finally(() => { + // No-op. + }); + } + }); + } + + private setCustomTokenAttributes( + redirectToConsole: boolean, + defaultOrg: { refLink: string }, + ) { + if (this.skipAuth) throw Error; + const linkOrgQuery = this.getOrgLinkFromQueryParams(defaultOrg); + const consoleCloudUrl = + this.appConfigService.getConfig().auth.consoleCloudUrl; + this.oauthService.customQueryParams = { + orgLink: linkOrgQuery, + targetUri: redirectToConsole ? consoleCloudUrl : window.location.href, + }; + if (redirectToConsole) { + // Redirect to console cloud because we dont know the tenant url, but console does + this.oauthService.redirectUri = consoleCloudUrl; } - - private getOrgLinkFromQueryParams(defaultOrg: { refLink: string }): string { - if (this.skipAuth) throw Error; - const params = new URLSearchParams(window.location.search); - const orgLinkUnderscored = params.get('org_link'); - const orgLinkBase = params.get('orgLink'); - if (orgLinkBase || orgLinkUnderscored) { - return [orgLinkBase, orgLinkUnderscored].find((el) => el); - } else { - return defaultOrg ? defaultOrg.refLink : this.appConfigService.getConfig().auth.orgLinkRoot; - } + } + + private getOrgLinkFromQueryParams(defaultOrg: { refLink: string }): string { + if (this.skipAuth) throw Error; + const params = new URLSearchParams(window.location.search); + const orgLinkUnderscored = params.get("org_link"); + const orgLinkBase = params.get("orgLink"); + if (orgLinkBase || orgLinkUnderscored) { + return [orgLinkBase, orgLinkUnderscored].find((el) => el); + } else { + return defaultOrg + ? defaultOrg.refLink + : this.appConfigService.getConfig().auth.orgLinkRoot; } + } - private static toMillis(seconds: number) { - return seconds * 1000; - } + private static toMillis(seconds: number) { + return seconds * 1000; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.module.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.module.ts index c96b1c4912..b8c5d511f0 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.module.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.module.ts @@ -3,101 +3,121 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule, APP_INITIALIZER } from '@angular/core'; -import { HTTP_INTERCEPTORS, HttpBackend, HttpClientModule } from '@angular/common/http'; -import { AppConfigService } from './app-config.service'; -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule, APP_INITIALIZER } from "@angular/core"; +import { + HTTP_INTERCEPTORS, + HttpBackend, + HttpClientModule, +} from "@angular/common/http"; +import { AppConfigService } from "./app-config.service"; +import { BrowserModule } from "@angular/platform-browser"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { AuthConfig, OAuthModule, OAuthModuleConfig, OAuthStorage } from 'angular-oauth2-oidc'; +import { + AuthConfig, + OAuthModule, + OAuthModuleConfig, + OAuthStorage, +} from "angular-oauth2-oidc"; -import { TimeagoModule } from 'ngx-timeago'; -import { LottieModule } from 'ngx-lottie'; +import { TimeagoModule } from "ngx-timeago"; +import { LottieModule } from "ngx-lottie"; -import { ApolloModule } from 'apollo-angular'; +import { ApolloModule } from "apollo-angular"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { VdkSharedCoreModule, VdkSharedFeaturesModule, VdkSharedNgRxModule, VdkSharedComponentsModule } from '@versatiledatakit/shared'; +import { + VdkSharedCoreModule, + VdkSharedFeaturesModule, + VdkSharedNgRxModule, + VdkSharedComponentsModule, +} from "@versatiledatakit/shared"; -import { VdkDataPipelinesModule } from '@versatiledatakit/data-pipelines'; +import { VdkDataPipelinesModule } from "@versatiledatakit/data-pipelines"; -import { AuthorizationInterceptor } from './http.interceptor'; +import { AuthorizationInterceptor } from "./http.interceptor"; -import { AppRouting } from './app.routing'; +import { AppRouting } from "./app.routing"; -import { AppComponent } from './app.component'; +import { AppComponent } from "./app.component"; -import { GettingStartedComponent } from './getting-started/getting-started.component'; -import { Router } from '@angular/router'; +import { GettingStartedComponent } from "./getting-started/getting-started.component"; +import { Router } from "@angular/router"; // eslint-disable-next-line prefer-arrow/prefer-arrow-functions export function lottiePlayerLoader() { - return import('lottie-web'); + return import("lottie-web"); } @NgModule({ - imports: [ - AppRouting, - BrowserModule, - ClarityModule, - BrowserAnimationsModule, - HttpClientModule, - OAuthModule.forRoot(), - ApolloModule, - TimeagoModule.forRoot(), - LottieModule.forRoot({ player: lottiePlayerLoader }), - VdkSharedCoreModule.forRoot(), - VdkSharedFeaturesModule.forRoot(), - VdkSharedNgRxModule.forRootWithDevtools(), - VdkSharedComponentsModule.forRoot(), - VdkDataPipelinesModule.forRoot({ - defaultOwnerTeamName: 'taurus', - manageConfig: { - allowKeyTabDownloads: true, - showTeamSectionInJobDetails: true - }, - exploreConfig: { - showTeamsColumn: true, - showTeamSectionInJobDetails: true - }, - healthStatusUrl: '/explore/data-jobs?search={0}', - showExecutionsPage: true, - showLineagePage: false, - dataPipelinesDocumentationUrl: '#' - }) - ], - declarations: [AppComponent, GettingStartedComponent], - providers: [ - { provide: AppConfigService, useClass: AppConfigService, deps: [HttpBackend, Router] }, - { - deps: [AppConfigService], - multi: true, - provide: APP_INITIALIZER, - useFactory: (appConfig: AppConfigService) => () => appConfig.loadAppConfig() - }, - { - provide: OAuthStorage, - useValue: localStorage - }, - { - deps: [AppConfigService], - provide: AuthConfig, - useFactory: (appConfig: AppConfigService) => () => appConfig.getAuthCodeFlowConfig() - }, - { - deps: [AppConfigService], - provide: OAuthModuleConfig, - useFactory: (appConfig: AppConfigService) => ({ - resourceServer: appConfig.getConfig().auth.resourceServer - }) - }, - { - multi: true, - provide: HTTP_INTERCEPTORS, - useClass: AuthorizationInterceptor - } - ], - bootstrap: [AppComponent] + imports: [ + AppRouting, + BrowserModule, + ClarityModule, + BrowserAnimationsModule, + HttpClientModule, + OAuthModule.forRoot(), + ApolloModule, + TimeagoModule.forRoot(), + LottieModule.forRoot({ player: lottiePlayerLoader }), + VdkSharedCoreModule.forRoot(), + VdkSharedFeaturesModule.forRoot(), + VdkSharedNgRxModule.forRootWithDevtools(), + VdkSharedComponentsModule.forRoot(), + VdkDataPipelinesModule.forRoot({ + defaultOwnerTeamName: "taurus", + manageConfig: { + allowKeyTabDownloads: true, + showTeamSectionInJobDetails: true, + }, + exploreConfig: { + showTeamsColumn: true, + showTeamSectionInJobDetails: true, + }, + healthStatusUrl: "/explore/data-jobs?search={0}", + showExecutionsPage: true, + showLineagePage: false, + dataPipelinesDocumentationUrl: "#", + }), + ], + declarations: [AppComponent, GettingStartedComponent], + providers: [ + { + provide: AppConfigService, + useClass: AppConfigService, + deps: [HttpBackend, Router], + }, + { + deps: [AppConfigService], + multi: true, + provide: APP_INITIALIZER, + useFactory: (appConfig: AppConfigService) => () => + appConfig.loadAppConfig(), + }, + { + provide: OAuthStorage, + useValue: localStorage, + }, + { + deps: [AppConfigService], + provide: AuthConfig, + useFactory: (appConfig: AppConfigService) => () => + appConfig.getAuthCodeFlowConfig(), + }, + { + deps: [AppConfigService], + provide: OAuthModuleConfig, + useFactory: (appConfig: AppConfigService) => ({ + resourceServer: appConfig.getConfig().auth.resourceServer, + }), + }, + { + multi: true, + provide: HTTP_INTERCEPTORS, + useClass: AuthorizationInterceptor, + }, + ], + bootstrap: [AppComponent], }) export class AppModule {} diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.routing.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.routing.ts index 3727a5f077..da2b903105 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.routing.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/app.routing.ts @@ -4,123 +4,123 @@ */ // modules -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; import { - DataJobDetailsPageComponent, - DataJobExecutionsPageComponent, - DataJobPageComponent, - DataJobsExplorePageComponent, - DataJobsManagePageComponent, - DataPipelinesRoutes -} from '@versatiledatakit/data-pipelines'; + DataJobDetailsPageComponent, + DataJobExecutionsPageComponent, + DataJobPageComponent, + DataJobsExplorePageComponent, + DataJobsManagePageComponent, + DataPipelinesRoutes, +} from "@versatiledatakit/data-pipelines"; -import { GettingStartedComponent } from './getting-started/getting-started.component'; -import { DataPipelinesRoute } from '../../../data-pipelines/src/lib/model'; +import { GettingStartedComponent } from "./getting-started/getting-started.component"; +import { DataPipelinesRoute } from "../../../data-pipelines/src/lib/model"; export const routes: DataPipelinesRoutes = [ - { path: 'get-started', component: GettingStartedComponent }, + { path: "get-started", component: GettingStartedComponent }, - //Explore - { - path: 'explore/data-jobs', - component: DataJobsExplorePageComponent, - data: { - navigateTo: { - path: '/explore/data-jobs/{0}/{1}', - replacers: [ - { searchValue: '{0}', replaceValue: '$.team' }, - { searchValue: '{1}', replaceValue: '$.job' } - ] - }, - restoreUiWhen: { - previousConfigPathLike: '/explore/data-jobs/:team/:job' - } - } + //Explore + { + path: "explore/data-jobs", + component: DataJobsExplorePageComponent, + data: { + navigateTo: { + path: "/explore/data-jobs/{0}/{1}", + replacers: [ + { searchValue: "{0}", replaceValue: "$.team" }, + { searchValue: "{1}", replaceValue: "$.job" }, + ], + }, + restoreUiWhen: { + previousConfigPathLike: "/explore/data-jobs/:team/:job", + }, + }, + }, + { + path: "explore/data-jobs/:team/:job", + component: DataJobPageComponent, + data: { + context: "explore", + teamParamKey: "team", + jobParamKey: "job", }, - { - path: 'explore/data-jobs/:team/:job', - component: DataJobPageComponent, + children: [ + { + path: "details", + component: DataJobDetailsPageComponent, data: { - context: 'explore', - teamParamKey: 'team', - jobParamKey: 'job' + editable: false, + navigateBack: { + path: "/explore/data-jobs", + }, }, - children: [ - { - path: 'details', - component: DataJobDetailsPageComponent, - data: { - editable: false, - navigateBack: { - path: '/explore/data-jobs' - } - } - }, - { - path: 'executions', - redirectTo: 'details' - }, - { path: '**', redirectTo: 'details' } - ] - }, + }, + { + path: "executions", + redirectTo: "details", + }, + { path: "**", redirectTo: "details" }, + ], + }, - //Manage - { - path: 'manage/data-jobs', - component: DataJobsManagePageComponent, - data: { - navigateTo: { - path: '/manage/data-jobs/{0}/{1}', - replacers: [ - { searchValue: '{0}', replaceValue: '$.team' }, - { searchValue: '{1}', replaceValue: '$.job' } - ] - }, - restoreUiWhen: { - previousConfigPathLike: '/manage/data-jobs/:team/:job' - } - } as DataPipelinesRoute + //Manage + { + path: "manage/data-jobs", + component: DataJobsManagePageComponent, + data: { + navigateTo: { + path: "/manage/data-jobs/{0}/{1}", + replacers: [ + { searchValue: "{0}", replaceValue: "$.team" }, + { searchValue: "{1}", replaceValue: "$.job" }, + ], + }, + restoreUiWhen: { + previousConfigPathLike: "/manage/data-jobs/:team/:job", + }, + } as DataPipelinesRoute, + }, + { + path: "manage/data-jobs/:team/:job", + component: DataJobPageComponent, + data: { + context: "manage", + teamParamKey: "team", + jobParamKey: "job", }, - { - path: 'manage/data-jobs/:team/:job', - component: DataJobPageComponent, + children: [ + { + path: "details", + component: DataJobDetailsPageComponent, data: { - context: 'manage', - teamParamKey: 'team', - jobParamKey: 'job' + editable: true, + navigateBack: { + path: "/manage/data-jobs", + }, }, - children: [ - { - path: 'details', - component: DataJobDetailsPageComponent, - data: { - editable: true, - navigateBack: { - path: '/manage/data-jobs' - } - } - }, - { - path: 'executions', - component: DataJobExecutionsPageComponent, - data: { - editable: true, - navigateBack: { - path: '/manage/data-jobs' - } - } - }, - { path: '**', redirectTo: 'details' } - ] - }, + }, + { + path: "executions", + component: DataJobExecutionsPageComponent, + data: { + editable: true, + navigateBack: { + path: "/manage/data-jobs", + }, + }, + }, + { path: "**", redirectTo: "details" }, + ], + }, - { path: '**', redirectTo: 'get-started' } + { path: "**", redirectTo: "get-started" }, ]; @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], }) export class AppRouting {} diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.html b/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.html index 37dcf36e22..474ff6508b 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.html +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.html @@ -4,107 +4,99 @@ -->
    -
    -

    - Get Started with Data Pipelines -

    -
    -
    -

    - Data Pipelines help Data Engineers develop, deploy, run, and manage - data processing workloads This app wraps the data pipelines library - in order to provide example of the library usage -

    -
    +
    +

    Get Started with Data Pipelines

    +
    +
    +

    + Data Pipelines help Data Engineers develop, deploy, run, and manage data + processing workloads This app wraps the data pipelines library in order to + provide example of the library usage +

    +
    -
    -
    -

    Learn more recommended

    +
    +
    +

    Learn more recommended

    -
    -
    -
    -
    -

    Overview

    -

    - High level architecture and capabilities of the data - pipelines project -

    -
    - -
    -
    -
    -
    -
    -

    How to

    -

    - Learn about how to create your first data job -

    -
    - -
    -
    -
    -
    -
    -

    Explore

    -

    - Explore existing data jobs -

    -
    - -
    -
    +
    +
    +
    +
    +

    Overview

    +

    + High level architecture and capabilities of the data pipelines + project +

    +
    + +
    +
    +
    +
    +
    +

    How to

    +

    + Learn about how to create your first data job +

    +
    +
    +
    +
    +
    +
    +

    Explore

    +

    + Explore existing data jobs +

    +
    + +
    +
    +
    -
    -
    -
    - -
    -
    +
    +
    +
    + +
    +
    -
    -
    -

    Widgets

    -
    -
    -
    - -
    -
    +
    +
    +

    Widgets

    +
    +
    +
    + +
    +
    diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.spec.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.spec.ts index 998e4aeb91..a6ededc678 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.spec.ts @@ -3,52 +3,58 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { GettingStartedComponent } from './getting-started.component'; -import { OAuthService } from 'angular-oauth2-oidc'; -import { AppConfigService } from '../app-config.service'; - -describe('GettingStartedComponent', () => { - let component: GettingStartedComponent; - let fixture: ComponentFixture; - let appConfigServiceStub: jasmine.SpyObj; - - beforeEach(waitForAsync(() => { - appConfigServiceStub = jasmine.createSpyObj('appConfigService', ['getConfig', 'getAuthCodeFlowConfig']); - TestBed.configureTestingModule({ - declarations: [GettingStartedComponent], - providers: [OAuthService, { provide: AppConfigService, useValue: appConfigServiceStub }], - imports: [] - }).compileComponents(); - })); - - it('should create without ignored components', () => { - appConfigServiceStub.getConfig.and.returnValue({ - auth: {}, - ignoreRoutes: [], - ignoreComponents: [] - }); - - fixture = TestBed.createComponent(GettingStartedComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - - expect(component).toBeTruthy(); - expect(component.widgetsVisible).toBeTrue(); +import { waitForAsync, ComponentFixture, TestBed } from "@angular/core/testing"; +import { GettingStartedComponent } from "./getting-started.component"; +import { OAuthService } from "angular-oauth2-oidc"; +import { AppConfigService } from "../app-config.service"; + +describe("GettingStartedComponent", () => { + let component: GettingStartedComponent; + let fixture: ComponentFixture; + let appConfigServiceStub: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + appConfigServiceStub = jasmine.createSpyObj( + "appConfigService", + ["getConfig", "getAuthCodeFlowConfig"], + ); + TestBed.configureTestingModule({ + declarations: [GettingStartedComponent], + providers: [ + OAuthService, + { provide: AppConfigService, useValue: appConfigServiceStub }, + ], + imports: [], + }).compileComponents(); + })); + + it("should create without ignored components", () => { + appConfigServiceStub.getConfig.and.returnValue({ + auth: {}, + ignoreRoutes: [], + ignoreComponents: [], }); - it('should create with widgets component ignored', () => { - appConfigServiceStub.getConfig.and.returnValue({ - auth: {}, - ignoreRoutes: [], - ignoreComponents: ['widgetsComponent'] - }); + fixture = TestBed.createComponent(GettingStartedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); - fixture = TestBed.createComponent(GettingStartedComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + expect(component).toBeTruthy(); + expect(component.widgetsVisible).toBeTrue(); + }); - expect(component).toBeTruthy(); - expect(component.widgetsVisible).toBeFalse(); + it("should create with widgets component ignored", () => { + appConfigServiceStub.getConfig.and.returnValue({ + auth: {}, + ignoreRoutes: [], + ignoreComponents: ["widgetsComponent"], }); + + fixture = TestBed.createComponent(GettingStartedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component).toBeTruthy(); + expect(component.widgetsVisible).toBeFalse(); + }); }); diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.ts index 56cb323f54..beb77dc29a 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/getting-started/getting-started.component.ts @@ -3,21 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; -import { AppConfigService } from '../app-config.service'; +import { Component } from "@angular/core"; +import { AppConfigService } from "../app-config.service"; @Component({ - selector: 'app-getting-started', - templateUrl: './getting-started.component.html', - styleUrls: ['./getting-started.component.scss'] + selector: "app-getting-started", + templateUrl: "./getting-started.component.html", + styleUrls: ["./getting-started.component.scss"], }) export class GettingStartedComponent { - constructor(private readonly appConfigService: AppConfigService) { - // No-op. - } + constructor(private readonly appConfigService: AppConfigService) { + // No-op. + } - get widgetsVisible(): boolean { - const ignoreComponents = this.appConfigService.getConfig().ignoreComponents; - return !(ignoreComponents && ignoreComponents.includes('widgetsComponent')); - } + get widgetsVisible(): boolean { + const ignoreComponents = this.appConfigService.getConfig().ignoreComponents; + return !(ignoreComponents && ignoreComponents.includes("widgetsComponent")); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/app/http.interceptor.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/app/http.interceptor.ts index c370cea0df..298c11b0dd 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/app/http.interceptor.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/app/http.interceptor.ts @@ -3,65 +3,86 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable, Optional } from '@angular/core'; -import { OAuthModuleConfig, OAuthResourceServerErrorHandler, OAuthStorage } from 'angular-oauth2-oidc'; -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable, Optional } from "@angular/core"; +import { + OAuthModuleConfig, + OAuthResourceServerErrorHandler, + OAuthStorage, +} from "angular-oauth2-oidc"; +import { + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from "@angular/common/http"; -import { Observable } from 'rxjs'; -import { AppConfigService } from './app-config.service'; +import { Observable } from "rxjs"; +import { AppConfigService } from "./app-config.service"; @Injectable() export class AuthorizationInterceptor implements HttpInterceptor { - constructor( - private readonly appConfigService: AppConfigService, - private authStorage: OAuthStorage, - private errorHandler: OAuthResourceServerErrorHandler, - @Optional() private moduleConfig: OAuthModuleConfig - ) {} + constructor( + private readonly appConfigService: AppConfigService, + private authStorage: OAuthStorage, + private errorHandler: OAuthResourceServerErrorHandler, + @Optional() private moduleConfig: OAuthModuleConfig, + ) {} - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (this.appConfigService.getSkipAuth()) { - return next.handle(req); - } - if (!this.moduleConfig) { - return next.handle(req); - } - if (!this.moduleConfig.resourceServer) { - return next.handle(req); - } - if (!this.moduleConfig.resourceServer.allowedUrls) { - return next.handle(req); - } - - const url = req.url.toLowerCase(); - if (!this.checkUrl(url)) { - return next.handle(req); - } + /** + * @inheritDoc + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { + if (this.appConfigService.getSkipAuth()) { + return next.handle(req); + } + if (!this.moduleConfig) { + return next.handle(req); + } + if (!this.moduleConfig.resourceServer) { + return next.handle(req); + } + if (!this.moduleConfig.resourceServer.allowedUrls) { + return next.handle(req); + } - const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; - const authCodeFlowConfig = this.appConfigService.getAuthCodeFlowConfig(); + const url = req.url.toLowerCase(); + if (!this.checkUrl(url)) { + return next.handle(req); + } - if (sendAccessToken && url.startsWith(authCodeFlowConfig.issuer) && url.endsWith('api/auth/token')) { - const headers = req.headers.set('Authorization', 'Basic ' + btoa(authCodeFlowConfig.clientId + ':')); + const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; + const authCodeFlowConfig = this.appConfigService.getAuthCodeFlowConfig(); - return next.handle(req.clone({ headers })); - } else if (sendAccessToken) { - const token = this.authStorage.getItem('access_token'); - const header = `Bearer ${token}`; - const headers = req.headers.set('Authorization', header); + if ( + sendAccessToken && + url.startsWith(authCodeFlowConfig.issuer) && + url.endsWith("api/auth/token") + ) { + const headers = req.headers.set( + "Authorization", + "Basic " + btoa(authCodeFlowConfig.clientId + ":"), + ); - return next.handle(req.clone({ headers })); - } + return next.handle(req.clone({ headers })); + } else if (sendAccessToken) { + const token = this.authStorage.getItem("access_token"); + const header = `Bearer ${token}`; + const headers = req.headers.set("Authorization", header); - return next.handle(req); + return next.handle(req.clone({ headers })); } - private checkUrl(url: string): boolean { - const found = this.moduleConfig.resourceServer.allowedUrls.find((u) => url.startsWith(u)); - return !!found; - } + return next.handle(req); + } + + private checkUrl(url: string): boolean { + const found = this.moduleConfig.resourceServer.allowedUrls.find((u) => + url.startsWith(u), + ); + return !!found; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/assets/css/clr-ui.min.css b/projects/frontend/data-pipelines/gui/projects/ui/src/assets/css/clr-ui.min.css index 78f8467d0a..78e812302e 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/assets/css/clr-ui.min.css +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/assets/css/clr-ui.min.css @@ -12,13 +12,13 @@ * */ /*! normalize.css v4.2.0 | MIT License | github.com/necolas/normalize.css */ html { - font-family: sans-serif; - line-height: 1.15; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; + font-family: sans-serif; + line-height: 1.15; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; } body { - margin: 0; + margin: 0; } article, aside, @@ -32,3187 +32,3187 @@ menu, nav, section, summary { - display: block; + display: block; } audio, canvas, progress, video { - display: inline-block; + display: inline-block; } audio:not([controls]) { - display: none; - height: 0; + display: none; + height: 0; } progress { - vertical-align: baseline; + vertical-align: baseline; } [hidden], template { - display: none; + display: none; } a { - background-color: transparent; - -webkit-text-decoration-skip: objects; + background-color: transparent; + -webkit-text-decoration-skip: objects; } a:active, a:hover { - outline-width: 0; + outline-width: 0; } abbr[title] { - border-bottom: none; - text-decoration: underline; - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; } b, strong { - font-weight: inherit; + font-weight: inherit; } b, strong { - font-weight: bolder; + font-weight: bolder; } dfn { - font-style: italic; + font-style: italic; } h1 { - font-size: 2em; - margin: 0.67em 0; + font-size: 2em; + margin: 0.67em 0; } mark { - background-color: #ff0; - color: #000; + background-color: #ff0; + color: #000; } small { - font-size: 80%; + font-size: 80%; } sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } sub { - bottom: -0.25em; + bottom: -0.25em; } sup { - top: -0.5em; + top: -0.5em; } img { - border-style: none; + border-style: none; } svg:not(:root) { - overflow: hidden; + overflow: hidden; } code, kbd, pre, samp { - font-family: monospace, monospace; - font-size: 1em; + font-family: monospace, monospace; + font-size: 1em; } figure { - margin: 1em 40px; + margin: 1em 40px; } hr { - -webkit-box-sizing: content-box; - box-sizing: content-box; - height: 0; - overflow: visible; + -webkit-box-sizing: content-box; + box-sizing: content-box; + height: 0; + overflow: visible; } button, input, optgroup, select, textarea { - font: inherit; - margin: 0; + font: inherit; + margin: 0; } optgroup { - font-weight: 700; + font-weight: 700; } button, input { - overflow: visible; + overflow: visible; } button, select { - text-transform: none; + text-transform: none; } -[type='reset'], -[type='submit'], +[type="reset"], +[type="submit"], button, -html [type='button'] { - -webkit-appearance: button; +html [type="button"] { + -webkit-appearance: button; } -[type='button']::-moz-focus-inner, -[type='reset']::-moz-focus-inner, -[type='submit']::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner, button::-moz-focus-inner { - border-style: none; - padding: 0; + border-style: none; + padding: 0; } -[type='button']:-moz-focusring, -[type='reset']:-moz-focusring, -[type='submit']:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring, button:-moz-focusring { - outline: 1px dotted ButtonText; + outline: 1px dotted ButtonText; } fieldset { - border: 1px solid silver; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; + border: 1px solid silver; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } legend { - -webkit-box-sizing: border-box; - box-sizing: border-box; - color: inherit; - display: table; - max-width: 100%; - padding: 0; - white-space: normal; + -webkit-box-sizing: border-box; + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; } textarea { - overflow: auto; + overflow: auto; } -[type='checkbox'], -[type='radio'] { - -webkit-box-sizing: border-box; - box-sizing: border-box; - padding: 0; +[type="checkbox"], +[type="radio"] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0; } -[type='number']::-webkit-inner-spin-button, -[type='number']::-webkit-outer-spin-button { - height: auto; +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; } -[type='search'] { - -webkit-appearance: textfield; - outline-offset: -2px; +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; } -[type='search']::-webkit-search-cancel-button, -[type='search']::-webkit-search-decoration { - -webkit-appearance: none; +[type="search"]::-webkit-search-cancel-button, +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; } ::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; + color: inherit; + opacity: 0.54; } ::-webkit-file-upload-button { - -webkit-appearance: button; - font: inherit; + -webkit-appearance: button; + font: inherit; } :root { - --clr-color-neutral-0: white; - --clr-color-neutral-50: #fafafa; - --clr-color-neutral-100: #f2f2f2; - --clr-color-neutral-200: #e8e8e8; - --clr-color-neutral-300: #dedede; - --clr-color-neutral-400: #cccccc; - --clr-color-neutral-500: #b3b3b3; - --clr-color-neutral-600: #8c8c8c; - --clr-color-neutral-700: #666666; - --clr-color-neutral-800: #454545; - --clr-color-neutral-900: #333333; - --clr-color-neutral-1000: black; - --clr-color-on-neutral-0: black; - --clr-color-on-neutral-50: black; - --clr-color-on-neutral-100: black; - --clr-color-on-neutral-200: black; - --clr-color-on-neutral-300: black; - --clr-color-on-neutral-400: black; - --clr-color-on-neutral-500: black; - --clr-color-on-neutral-600: white; - --clr-color-on-neutral-700: white; - --clr-color-on-neutral-800: white; - --clr-color-on-neutral-900: white; - --clr-color-on-neutral-1000: white; - --clr-color-action-50: #e3f5fc; - --clr-color-action-100: #c8eaf9; - --clr-color-action-200: #9bd8f3; - --clr-color-action-300: #79c6e6; - --clr-color-action-400: #49aeda; - --clr-color-action-500: #179bd3; - --clr-color-action-600: #0072a3; - --clr-color-action-700: #00648f; - --clr-color-action-800: #00567a; - --clr-color-action-900: #004b6b; - --clr-color-action-1000: #00364d; - --clr-color-on-action-50: black; - --clr-color-on-action-100: black; - --clr-color-on-action-200: black; - --clr-color-on-action-300: black; - --clr-color-on-action-400: black; - --clr-color-on-action-500: black; - --clr-color-on-action-600: white; - --clr-color-on-action-700: white; - --clr-color-on-action-800: white; - --clr-color-on-action-900: white; - --clr-color-on-action-1000: white; - --clr-color-secondary-action-50: #f7e6ff; - --clr-color-secondary-action-100: #e6caf1; - --clr-color-secondary-action-200: #d2aae4; - --clr-color-secondary-action-300: #c090d5; - --clr-color-secondary-action-400: #af73c9; - --clr-color-secondary-action-500: #9e57bc; - --clr-color-secondary-action-600: #8a39ac; - --clr-color-secondary-action-700: #781d9f; - --clr-color-secondary-action-800: #680094; - --clr-color-secondary-action-900: #4f0070; - --clr-color-secondary-action-1000: #320047; - --clr-color-on-secondary-action-50: black; - --clr-color-on-secondary-action-100: black; - --clr-color-on-secondary-action-200: black; - --clr-color-on-secondary-action-300: black; - --clr-color-on-secondary-action-400: black; - --clr-color-on-secondary-action-500: white; - --clr-color-on-secondary-action-600: white; - --clr-color-on-secondary-action-700: white; - --clr-color-on-secondary-action-800: white; - --clr-color-on-secondary-action-900: white; - --clr-color-on-secondary-action-1000: white; - --clr-color-danger-50: #fff2f0; - --clr-color-danger-100: #feddd7; - --clr-color-danger-200: #fcc5bb; - --clr-color-danger-300: #f59e8f; - --clr-color-danger-400: #f27963; - --clr-color-danger-500: #f35e44; - --clr-color-danger-600: #f52d0a; - --clr-color-danger-700: #db2100; - --clr-color-danger-800: #c21d00; - --clr-color-danger-900: #991700; - --clr-color-danger-1000: #660f00; - --clr-color-on-danger-50: black; - --clr-color-on-danger-100: black; - --clr-color-on-danger-200: black; - --clr-color-on-danger-300: black; - --clr-color-on-danger-400: black; - --clr-color-on-danger-500: black; - --clr-color-on-danger-600: black; - --clr-color-on-danger-700: white; - --clr-color-on-danger-800: white; - --clr-color-on-danger-900: white; - --clr-color-on-danger-1000: white; - --clr-color-warning-50: #fffae6; - --clr-color-warning-100: #fff4c7; - --clr-color-warning-200: #ffeea8; - --clr-color-warning-300: #fee272; - --clr-color-warning-400: #f8cf2a; - --clr-color-warning-500: #efc006; - --clr-color-warning-600: #e6b000; - --clr-color-warning-700: #d69a00; - --clr-color-warning-800: #ad7600; - --clr-color-warning-900: #8f5a00; - --clr-color-warning-1000: #613200; - --clr-color-on-warning-50: black; - --clr-color-on-warning-100: black; - --clr-color-on-warning-200: black; - --clr-color-on-warning-300: black; - --clr-color-on-warning-400: black; - --clr-color-on-warning-500: black; - --clr-color-on-warning-600: black; - --clr-color-on-warning-700: black; - --clr-color-on-warning-800: black; - --clr-color-on-warning-900: black; - --clr-color-on-warning-1000: white; - --clr-color-success-50: #dff0d0; - --clr-color-success-100: #bce49a; - --clr-color-success-200: #73dc1e; - --clr-color-success-300: #68c71a; - --clr-color-success-400: #5eb715; - --clr-color-success-500: #5aa220; - --clr-color-success-600: #4b970c; - --clr-color-success-700: #3c8500; - --clr-color-success-800: #306b00; - --clr-color-success-900: #255200; - --clr-color-success-1000: #1e4200; - --clr-color-on-success-50: black; - --clr-color-on-success-100: black; - --clr-color-on-success-200: black; - --clr-color-on-success-300: black; - --clr-color-on-success-400: black; - --clr-color-on-success-500: black; - --clr-color-on-success-600: black; - --clr-color-on-success-700: white; - --clr-color-on-success-800: white; - --clr-color-on-success-900: white; - --clr-color-on-success-1000: white; + --clr-color-neutral-0: white; + --clr-color-neutral-50: #fafafa; + --clr-color-neutral-100: #f2f2f2; + --clr-color-neutral-200: #e8e8e8; + --clr-color-neutral-300: #dedede; + --clr-color-neutral-400: #cccccc; + --clr-color-neutral-500: #b3b3b3; + --clr-color-neutral-600: #8c8c8c; + --clr-color-neutral-700: #666666; + --clr-color-neutral-800: #454545; + --clr-color-neutral-900: #333333; + --clr-color-neutral-1000: black; + --clr-color-on-neutral-0: black; + --clr-color-on-neutral-50: black; + --clr-color-on-neutral-100: black; + --clr-color-on-neutral-200: black; + --clr-color-on-neutral-300: black; + --clr-color-on-neutral-400: black; + --clr-color-on-neutral-500: black; + --clr-color-on-neutral-600: white; + --clr-color-on-neutral-700: white; + --clr-color-on-neutral-800: white; + --clr-color-on-neutral-900: white; + --clr-color-on-neutral-1000: white; + --clr-color-action-50: #e3f5fc; + --clr-color-action-100: #c8eaf9; + --clr-color-action-200: #9bd8f3; + --clr-color-action-300: #79c6e6; + --clr-color-action-400: #49aeda; + --clr-color-action-500: #179bd3; + --clr-color-action-600: #0072a3; + --clr-color-action-700: #00648f; + --clr-color-action-800: #00567a; + --clr-color-action-900: #004b6b; + --clr-color-action-1000: #00364d; + --clr-color-on-action-50: black; + --clr-color-on-action-100: black; + --clr-color-on-action-200: black; + --clr-color-on-action-300: black; + --clr-color-on-action-400: black; + --clr-color-on-action-500: black; + --clr-color-on-action-600: white; + --clr-color-on-action-700: white; + --clr-color-on-action-800: white; + --clr-color-on-action-900: white; + --clr-color-on-action-1000: white; + --clr-color-secondary-action-50: #f7e6ff; + --clr-color-secondary-action-100: #e6caf1; + --clr-color-secondary-action-200: #d2aae4; + --clr-color-secondary-action-300: #c090d5; + --clr-color-secondary-action-400: #af73c9; + --clr-color-secondary-action-500: #9e57bc; + --clr-color-secondary-action-600: #8a39ac; + --clr-color-secondary-action-700: #781d9f; + --clr-color-secondary-action-800: #680094; + --clr-color-secondary-action-900: #4f0070; + --clr-color-secondary-action-1000: #320047; + --clr-color-on-secondary-action-50: black; + --clr-color-on-secondary-action-100: black; + --clr-color-on-secondary-action-200: black; + --clr-color-on-secondary-action-300: black; + --clr-color-on-secondary-action-400: black; + --clr-color-on-secondary-action-500: white; + --clr-color-on-secondary-action-600: white; + --clr-color-on-secondary-action-700: white; + --clr-color-on-secondary-action-800: white; + --clr-color-on-secondary-action-900: white; + --clr-color-on-secondary-action-1000: white; + --clr-color-danger-50: #fff2f0; + --clr-color-danger-100: #feddd7; + --clr-color-danger-200: #fcc5bb; + --clr-color-danger-300: #f59e8f; + --clr-color-danger-400: #f27963; + --clr-color-danger-500: #f35e44; + --clr-color-danger-600: #f52d0a; + --clr-color-danger-700: #db2100; + --clr-color-danger-800: #c21d00; + --clr-color-danger-900: #991700; + --clr-color-danger-1000: #660f00; + --clr-color-on-danger-50: black; + --clr-color-on-danger-100: black; + --clr-color-on-danger-200: black; + --clr-color-on-danger-300: black; + --clr-color-on-danger-400: black; + --clr-color-on-danger-500: black; + --clr-color-on-danger-600: black; + --clr-color-on-danger-700: white; + --clr-color-on-danger-800: white; + --clr-color-on-danger-900: white; + --clr-color-on-danger-1000: white; + --clr-color-warning-50: #fffae6; + --clr-color-warning-100: #fff4c7; + --clr-color-warning-200: #ffeea8; + --clr-color-warning-300: #fee272; + --clr-color-warning-400: #f8cf2a; + --clr-color-warning-500: #efc006; + --clr-color-warning-600: #e6b000; + --clr-color-warning-700: #d69a00; + --clr-color-warning-800: #ad7600; + --clr-color-warning-900: #8f5a00; + --clr-color-warning-1000: #613200; + --clr-color-on-warning-50: black; + --clr-color-on-warning-100: black; + --clr-color-on-warning-200: black; + --clr-color-on-warning-300: black; + --clr-color-on-warning-400: black; + --clr-color-on-warning-500: black; + --clr-color-on-warning-600: black; + --clr-color-on-warning-700: black; + --clr-color-on-warning-800: black; + --clr-color-on-warning-900: black; + --clr-color-on-warning-1000: white; + --clr-color-success-50: #dff0d0; + --clr-color-success-100: #bce49a; + --clr-color-success-200: #73dc1e; + --clr-color-success-300: #68c71a; + --clr-color-success-400: #5eb715; + --clr-color-success-500: #5aa220; + --clr-color-success-600: #4b970c; + --clr-color-success-700: #3c8500; + --clr-color-success-800: #306b00; + --clr-color-success-900: #255200; + --clr-color-success-1000: #1e4200; + --clr-color-on-success-50: black; + --clr-color-on-success-100: black; + --clr-color-on-success-200: black; + --clr-color-on-success-300: black; + --clr-color-on-success-400: black; + --clr-color-on-success-500: black; + --clr-color-on-success-600: black; + --clr-color-on-success-700: white; + --clr-color-on-success-800: white; + --clr-color-on-success-900: white; + --clr-color-on-success-1000: white; } :root { - --clr-global-borderradius: 0.15rem; - --clr-global-borderwidth: 0.05rem; - --clr-global-app-background: #fafafa; - --clr-global-selection-color: #d8e3e9; - --clr-global-on-selection-color: black; - --clr-global-hover-color: #e8e8e8; - --clr-global-content-header-font-color: black; - --clr-global-font-color: #666666; - --clr-global-success-color: #5aa220; - --clr-global-error-color: #c21d00; - --clr-close-color--normal: #8c8c8c; - --clr-close-color--normal-opacity: 0.2; - --clr-close-color--hover: black; - --clr-close-color--hover-opacity: 0.5; - --clr-popover-box-shadow-color: rgba(140, 140, 140, 0.25); + --clr-global-borderradius: 0.15rem; + --clr-global-borderwidth: 0.05rem; + --clr-global-app-background: #fafafa; + --clr-global-selection-color: #d8e3e9; + --clr-global-on-selection-color: black; + --clr-global-hover-color: #e8e8e8; + --clr-global-content-header-font-color: black; + --clr-global-font-color: #666666; + --clr-global-success-color: #5aa220; + --clr-global-error-color: #c21d00; + --clr-close-color--normal: #8c8c8c; + --clr-close-color--normal-opacity: 0.2; + --clr-close-color--hover: black; + --clr-close-color--hover-opacity: 0.5; + --clr-popover-box-shadow-color: rgba(140, 140, 140, 0.25); } :root { - --clr-font: Metropolis, Avenir Next, Helvetica Neue, Arial, sans-serif; - --clr-display-font: var(--clr-font); - --clr-font-weight-light: 200; - --clr-font-weight-regular: 400; - --clr-font-weight-semibold: 500; - --clr-font-weight-bold: 600; - --clr-font-weight-extrabold: 600; - --clr-h1-color: var(--clr-global-content-header-font-color); - --clr-h1-font-weight: var(--clr-font-weight-light); - --clr-h1-font-family: var(--clr-display-font); - --clr-h2-color: var(--clr-global-content-header-font-color); - --clr-h2-font-weight: var(--clr-font-weight-light); - --clr-h2-font-family: var(--clr-display-font); - --clr-h3-color: var(--clr-global-content-header-font-color); - --clr-h3-font-weight: var(--clr-font-weight-light); - --clr-h3-font-family: var(--clr-display-font); - --clr-h4-color: var(--clr-global-content-header-font-color); - --clr-h4-font-weight: var(--clr-font-weight-light); - --clr-h4-font-family: var(--clr-display-font); - --clr-h5-color: var(--clr-global-font-color); - --clr-h5-font-weight: var(--clr-font-weight-regular); - --clr-h5-font-family: var(--clr-display-font); - --clr-h6-color: var(--clr-color-neutral-900); - --clr-h6-font-weight: var(--clr-font-weight-semibold); - --clr-h6-font-family: var(--clr-display-font); - --clr-p0-color: var(--clr-global-font-color); - --clr-p0-font-weight: var(--clr-font-weight-light); - --clr-p1-color: var(--clr-global-font-color); - --clr-p1-font-weight: var(--clr-font-weight-regular); - --clr-p2-color: var(--clr-global-font-color); - --clr-p2-font-weight: var(--clr-font-weight-semibold); - --clr-p3-color: var(--clr-global-font-color); - --clr-p3-font-weight: var(--clr-font-weight-regular); - --clr-p4-color: var(--clr-global-font-color); - --clr-p4-font-weight: var(--clr-font-weight-bold); - --clr-p5-color: var(--clr-global-font-color); - --clr-p5-font-weight: var(--clr-font-weight-regular); - --clr-p6-color: var(--clr-global-font-color); - --clr-p6-font-weight: var(--clr-font-weight-bold); - --clr-p7-color: var(--clr-global-font-color); - --clr-p7-font-weight: var(--clr-font-weight-regular); - --clr-p8-color: var(--clr-global-font-color); - --clr-p8-font-weight: var(--clr-font-weight-regular); + --clr-font: Metropolis, Avenir Next, Helvetica Neue, Arial, sans-serif; + --clr-display-font: var(--clr-font); + --clr-font-weight-light: 200; + --clr-font-weight-regular: 400; + --clr-font-weight-semibold: 500; + --clr-font-weight-bold: 600; + --clr-font-weight-extrabold: 600; + --clr-h1-color: var(--clr-global-content-header-font-color); + --clr-h1-font-weight: var(--clr-font-weight-light); + --clr-h1-font-family: var(--clr-display-font); + --clr-h2-color: var(--clr-global-content-header-font-color); + --clr-h2-font-weight: var(--clr-font-weight-light); + --clr-h2-font-family: var(--clr-display-font); + --clr-h3-color: var(--clr-global-content-header-font-color); + --clr-h3-font-weight: var(--clr-font-weight-light); + --clr-h3-font-family: var(--clr-display-font); + --clr-h4-color: var(--clr-global-content-header-font-color); + --clr-h4-font-weight: var(--clr-font-weight-light); + --clr-h4-font-family: var(--clr-display-font); + --clr-h5-color: var(--clr-global-font-color); + --clr-h5-font-weight: var(--clr-font-weight-regular); + --clr-h5-font-family: var(--clr-display-font); + --clr-h6-color: var(--clr-color-neutral-900); + --clr-h6-font-weight: var(--clr-font-weight-semibold); + --clr-h6-font-family: var(--clr-display-font); + --clr-p0-color: var(--clr-global-font-color); + --clr-p0-font-weight: var(--clr-font-weight-light); + --clr-p1-color: var(--clr-global-font-color); + --clr-p1-font-weight: var(--clr-font-weight-regular); + --clr-p2-color: var(--clr-global-font-color); + --clr-p2-font-weight: var(--clr-font-weight-semibold); + --clr-p3-color: var(--clr-global-font-color); + --clr-p3-font-weight: var(--clr-font-weight-regular); + --clr-p4-color: var(--clr-global-font-color); + --clr-p4-font-weight: var(--clr-font-weight-bold); + --clr-p5-color: var(--clr-global-font-color); + --clr-p5-font-weight: var(--clr-font-weight-regular); + --clr-p6-color: var(--clr-global-font-color); + --clr-p6-font-weight: var(--clr-font-weight-bold); + --clr-p7-color: var(--clr-global-font-color); + --clr-p7-font-weight: var(--clr-font-weight-regular); + --clr-p8-color: var(--clr-global-font-color); + --clr-p8-font-weight: var(--clr-font-weight-regular); } .clr-align-baseline { - vertical-align: baseline !important; + vertical-align: baseline !important; } .clr-align-top { - vertical-align: top !important; + vertical-align: top !important; } .clr-align-middle { - vertical-align: middle !important; + vertical-align: middle !important; } .clr-align-bottom { - vertical-align: bottom !important; + vertical-align: bottom !important; } .clr-align-text-bottom { - vertical-align: text-bottom !important; + vertical-align: text-bottom !important; } .clr-align-text-top { - vertical-align: text-top !important; + vertical-align: text-top !important; } .clr-clearfix::after { - content: ''; - display: table; - clear: both; + content: ""; + display: table; + clear: both; } .clr-display-block { - display: block !important; + display: block !important; } .clr-display-inline-block { - display: inline-block !important; + display: inline-block !important; } .clr-display-inline { - display: inline !important; + display: inline !important; } .clr-flex-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; +} +.clr-flex-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; +} +.clr-flex-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; +} +.clr-flex-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; +} +.clr-flex-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; +} +.clr-flex-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; +} +.clr-flex-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; +} +.clr-flex-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; +} +.clr-flex-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; +} +.clr-flex-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; +} +.clr-flex-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; +} +.clr-flex-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; +} +.clr-justify-content-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; +} +.clr-justify-content-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; +} +.clr-justify-content-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; +} +.clr-justify-content-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; +} +.clr-justify-content-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; +} +.clr-align-items-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; +} +.clr-align-items-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; +} +.clr-align-items-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; +} +.clr-align-items-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; +} +.clr-align-items-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; +} +.clr-align-content-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; +} +.clr-align-content-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; +} +.clr-align-content-center { + -ms-flex-line-pack: center !important; + align-content: center !important; +} +.clr-align-content-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; +} +.clr-align-content-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; +} +.clr-align-content-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; +} +.clr-align-self-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; +} +.clr-align-self-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; +} +.clr-align-self-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; +} +.clr-align-self-center { + -ms-flex-item-align: center !important; + align-self: center !important; +} +.clr-align-self-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; +} +.clr-align-self-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; +} +@media (min-width: 576px) { + .clr-flex-sm-row { -webkit-box-orient: horizontal !important; -webkit-box-direction: normal !important; -ms-flex-direction: row !important; flex-direction: row !important; -} -.clr-flex-column { + } + .clr-flex-sm-column { -webkit-box-orient: vertical !important; -webkit-box-direction: normal !important; -ms-flex-direction: column !important; flex-direction: column !important; -} -.clr-flex-row-reverse { + } + .clr-flex-sm-row-reverse { -webkit-box-orient: horizontal !important; -webkit-box-direction: reverse !important; -ms-flex-direction: row-reverse !important; flex-direction: row-reverse !important; -} -.clr-flex-column-reverse { + } + .clr-flex-sm-column-reverse { -webkit-box-orient: vertical !important; -webkit-box-direction: reverse !important; -ms-flex-direction: column-reverse !important; flex-direction: column-reverse !important; -} -.clr-flex-wrap { + } + .clr-flex-sm-wrap { -ms-flex-wrap: wrap !important; flex-wrap: wrap !important; -} -.clr-flex-nowrap { + } + .clr-flex-sm-nowrap { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; -} -.clr-flex-wrap-reverse { + } + .clr-flex-sm-wrap-reverse { -ms-flex-wrap: wrap-reverse !important; flex-wrap: wrap-reverse !important; -} -.clr-flex-fill { + } + .clr-flex-sm-fill { -webkit-box-flex: 1 !important; -ms-flex: 1 1 auto !important; flex: 1 1 auto !important; -} -.clr-flex-grow-0 { + } + .clr-flex-sm-grow-0 { -webkit-box-flex: 0 !important; -ms-flex-positive: 0 !important; flex-grow: 0 !important; -} -.clr-flex-grow-1 { + } + .clr-flex-sm-grow-1 { -webkit-box-flex: 1 !important; -ms-flex-positive: 1 !important; flex-grow: 1 !important; -} -.clr-flex-shrink-0 { + } + .clr-flex-sm-shrink-0 { -ms-flex-negative: 0 !important; flex-shrink: 0 !important; -} -.clr-flex-shrink-1 { + } + .clr-flex-sm-shrink-1 { -ms-flex-negative: 1 !important; flex-shrink: 1 !important; -} -.clr-justify-content-start { + } + .clr-justify-content-sm-start { -webkit-box-pack: start !important; -ms-flex-pack: start !important; justify-content: flex-start !important; -} -.clr-justify-content-end { + } + .clr-justify-content-sm-end { -webkit-box-pack: end !important; -ms-flex-pack: end !important; justify-content: flex-end !important; -} -.clr-justify-content-center { + } + .clr-justify-content-sm-center { -webkit-box-pack: center !important; -ms-flex-pack: center !important; justify-content: center !important; -} -.clr-justify-content-between { + } + .clr-justify-content-sm-between { -webkit-box-pack: justify !important; -ms-flex-pack: justify !important; justify-content: space-between !important; -} -.clr-justify-content-around { + } + .clr-justify-content-sm-around { -ms-flex-pack: distribute !important; justify-content: space-around !important; -} -.clr-align-items-start { + } + .clr-align-items-sm-start { -webkit-box-align: start !important; -ms-flex-align: start !important; align-items: flex-start !important; -} -.clr-align-items-end { + } + .clr-align-items-sm-end { -webkit-box-align: end !important; -ms-flex-align: end !important; align-items: flex-end !important; -} -.clr-align-items-center { + } + .clr-align-items-sm-center { -webkit-box-align: center !important; -ms-flex-align: center !important; align-items: center !important; -} -.clr-align-items-baseline { + } + .clr-align-items-sm-baseline { -webkit-box-align: baseline !important; -ms-flex-align: baseline !important; align-items: baseline !important; -} -.clr-align-items-stretch { + } + .clr-align-items-sm-stretch { -webkit-box-align: stretch !important; -ms-flex-align: stretch !important; align-items: stretch !important; -} -.clr-align-content-start { + } + .clr-align-content-sm-start { -ms-flex-line-pack: start !important; align-content: flex-start !important; -} -.clr-align-content-end { + } + .clr-align-content-sm-end { -ms-flex-line-pack: end !important; align-content: flex-end !important; -} -.clr-align-content-center { + } + .clr-align-content-sm-center { -ms-flex-line-pack: center !important; align-content: center !important; -} -.clr-align-content-between { + } + .clr-align-content-sm-between { -ms-flex-line-pack: justify !important; align-content: space-between !important; -} -.clr-align-content-around { + } + .clr-align-content-sm-around { -ms-flex-line-pack: distribute !important; align-content: space-around !important; -} -.clr-align-content-stretch { + } + .clr-align-content-sm-stretch { -ms-flex-line-pack: stretch !important; align-content: stretch !important; -} -.clr-align-self-auto { + } + .clr-align-self-sm-auto { -ms-flex-item-align: auto !important; align-self: auto !important; -} -.clr-align-self-start { + } + .clr-align-self-sm-start { -ms-flex-item-align: start !important; align-self: flex-start !important; -} -.clr-align-self-end { + } + .clr-align-self-sm-end { -ms-flex-item-align: end !important; align-self: flex-end !important; -} -.clr-align-self-center { + } + .clr-align-self-sm-center { -ms-flex-item-align: center !important; align-self: center !important; -} -.clr-align-self-baseline { + } + .clr-align-self-sm-baseline { -ms-flex-item-align: baseline !important; align-self: baseline !important; -} -.clr-align-self-stretch { + } + .clr-align-self-sm-stretch { -ms-flex-item-align: stretch !important; align-self: stretch !important; -} -@media (min-width: 576px) { - .clr-flex-sm-row { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: row !important; - flex-direction: row !important; - } - .clr-flex-sm-column { - -webkit-box-orient: vertical !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: column !important; - flex-direction: column !important; - } - .clr-flex-sm-row-reverse { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: row-reverse !important; - flex-direction: row-reverse !important; - } - .clr-flex-sm-column-reverse { - -webkit-box-orient: vertical !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: column-reverse !important; - flex-direction: column-reverse !important; - } - .clr-flex-sm-wrap { - -ms-flex-wrap: wrap !important; - flex-wrap: wrap !important; - } - .clr-flex-sm-nowrap { - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - } - .clr-flex-sm-wrap-reverse { - -ms-flex-wrap: wrap-reverse !important; - flex-wrap: wrap-reverse !important; - } - .clr-flex-sm-fill { - -webkit-box-flex: 1 !important; - -ms-flex: 1 1 auto !important; - flex: 1 1 auto !important; - } - .clr-flex-sm-grow-0 { - -webkit-box-flex: 0 !important; - -ms-flex-positive: 0 !important; - flex-grow: 0 !important; - } - .clr-flex-sm-grow-1 { - -webkit-box-flex: 1 !important; - -ms-flex-positive: 1 !important; - flex-grow: 1 !important; - } - .clr-flex-sm-shrink-0 { - -ms-flex-negative: 0 !important; - flex-shrink: 0 !important; - } - .clr-flex-sm-shrink-1 { - -ms-flex-negative: 1 !important; - flex-shrink: 1 !important; - } - .clr-justify-content-sm-start { - -webkit-box-pack: start !important; - -ms-flex-pack: start !important; - justify-content: flex-start !important; - } - .clr-justify-content-sm-end { - -webkit-box-pack: end !important; - -ms-flex-pack: end !important; - justify-content: flex-end !important; - } - .clr-justify-content-sm-center { - -webkit-box-pack: center !important; - -ms-flex-pack: center !important; - justify-content: center !important; - } - .clr-justify-content-sm-between { - -webkit-box-pack: justify !important; - -ms-flex-pack: justify !important; - justify-content: space-between !important; - } - .clr-justify-content-sm-around { - -ms-flex-pack: distribute !important; - justify-content: space-around !important; - } - .clr-align-items-sm-start { - -webkit-box-align: start !important; - -ms-flex-align: start !important; - align-items: flex-start !important; - } - .clr-align-items-sm-end { - -webkit-box-align: end !important; - -ms-flex-align: end !important; - align-items: flex-end !important; - } - .clr-align-items-sm-center { - -webkit-box-align: center !important; - -ms-flex-align: center !important; - align-items: center !important; - } - .clr-align-items-sm-baseline { - -webkit-box-align: baseline !important; - -ms-flex-align: baseline !important; - align-items: baseline !important; - } - .clr-align-items-sm-stretch { - -webkit-box-align: stretch !important; - -ms-flex-align: stretch !important; - align-items: stretch !important; - } - .clr-align-content-sm-start { - -ms-flex-line-pack: start !important; - align-content: flex-start !important; - } - .clr-align-content-sm-end { - -ms-flex-line-pack: end !important; - align-content: flex-end !important; - } - .clr-align-content-sm-center { - -ms-flex-line-pack: center !important; - align-content: center !important; - } - .clr-align-content-sm-between { - -ms-flex-line-pack: justify !important; - align-content: space-between !important; - } - .clr-align-content-sm-around { - -ms-flex-line-pack: distribute !important; - align-content: space-around !important; - } - .clr-align-content-sm-stretch { - -ms-flex-line-pack: stretch !important; - align-content: stretch !important; - } - .clr-align-self-sm-auto { - -ms-flex-item-align: auto !important; - align-self: auto !important; - } - .clr-align-self-sm-start { - -ms-flex-item-align: start !important; - align-self: flex-start !important; - } - .clr-align-self-sm-end { - -ms-flex-item-align: end !important; - align-self: flex-end !important; - } - .clr-align-self-sm-center { - -ms-flex-item-align: center !important; - align-self: center !important; - } - .clr-align-self-sm-baseline { - -ms-flex-item-align: baseline !important; - align-self: baseline !important; - } - .clr-align-self-sm-stretch { - -ms-flex-item-align: stretch !important; - align-self: stretch !important; - } + } } @media (min-width: 768px) { - .clr-flex-md-row { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: row !important; - flex-direction: row !important; - } - .clr-flex-md-column { - -webkit-box-orient: vertical !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: column !important; - flex-direction: column !important; - } - .clr-flex-md-row-reverse { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: row-reverse !important; - flex-direction: row-reverse !important; - } - .clr-flex-md-column-reverse { - -webkit-box-orient: vertical !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: column-reverse !important; - flex-direction: column-reverse !important; - } - .clr-flex-md-wrap { - -ms-flex-wrap: wrap !important; - flex-wrap: wrap !important; - } - .clr-flex-md-nowrap { - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - } - .clr-flex-md-wrap-reverse { - -ms-flex-wrap: wrap-reverse !important; - flex-wrap: wrap-reverse !important; - } - .clr-flex-md-fill { - -webkit-box-flex: 1 !important; - -ms-flex: 1 1 auto !important; - flex: 1 1 auto !important; - } - .clr-flex-md-grow-0 { - -webkit-box-flex: 0 !important; - -ms-flex-positive: 0 !important; - flex-grow: 0 !important; - } - .clr-flex-md-grow-1 { - -webkit-box-flex: 1 !important; - -ms-flex-positive: 1 !important; - flex-grow: 1 !important; - } - .clr-flex-md-shrink-0 { - -ms-flex-negative: 0 !important; - flex-shrink: 0 !important; - } - .clr-flex-md-shrink-1 { - -ms-flex-negative: 1 !important; - flex-shrink: 1 !important; - } - .clr-justify-content-md-start { - -webkit-box-pack: start !important; - -ms-flex-pack: start !important; - justify-content: flex-start !important; - } - .clr-justify-content-md-end { - -webkit-box-pack: end !important; - -ms-flex-pack: end !important; - justify-content: flex-end !important; - } - .clr-justify-content-md-center { - -webkit-box-pack: center !important; - -ms-flex-pack: center !important; - justify-content: center !important; - } - .clr-justify-content-md-between { - -webkit-box-pack: justify !important; - -ms-flex-pack: justify !important; - justify-content: space-between !important; - } - .clr-justify-content-md-around { - -ms-flex-pack: distribute !important; - justify-content: space-around !important; - } - .clr-align-items-md-start { - -webkit-box-align: start !important; - -ms-flex-align: start !important; - align-items: flex-start !important; - } - .clr-align-items-md-end { - -webkit-box-align: end !important; - -ms-flex-align: end !important; - align-items: flex-end !important; - } - .clr-align-items-md-center { - -webkit-box-align: center !important; - -ms-flex-align: center !important; - align-items: center !important; - } - .clr-align-items-md-baseline { - -webkit-box-align: baseline !important; - -ms-flex-align: baseline !important; - align-items: baseline !important; - } - .clr-align-items-md-stretch { - -webkit-box-align: stretch !important; - -ms-flex-align: stretch !important; - align-items: stretch !important; - } - .clr-align-content-md-start { - -ms-flex-line-pack: start !important; - align-content: flex-start !important; - } - .clr-align-content-md-end { - -ms-flex-line-pack: end !important; - align-content: flex-end !important; - } - .clr-align-content-md-center { - -ms-flex-line-pack: center !important; - align-content: center !important; - } - .clr-align-content-md-between { - -ms-flex-line-pack: justify !important; - align-content: space-between !important; - } - .clr-align-content-md-around { - -ms-flex-line-pack: distribute !important; - align-content: space-around !important; - } - .clr-align-content-md-stretch { - -ms-flex-line-pack: stretch !important; - align-content: stretch !important; - } - .clr-align-self-md-auto { - -ms-flex-item-align: auto !important; - align-self: auto !important; - } - .clr-align-self-md-start { - -ms-flex-item-align: start !important; - align-self: flex-start !important; - } - .clr-align-self-md-end { - -ms-flex-item-align: end !important; - align-self: flex-end !important; - } - .clr-align-self-md-center { - -ms-flex-item-align: center !important; - align-self: center !important; - } - .clr-align-self-md-baseline { - -ms-flex-item-align: baseline !important; - align-self: baseline !important; - } - .clr-align-self-md-stretch { - -ms-flex-item-align: stretch !important; - align-self: stretch !important; - } + .clr-flex-md-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .clr-flex-md-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .clr-flex-md-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .clr-flex-md-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .clr-flex-md-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .clr-flex-md-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .clr-flex-md-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .clr-flex-md-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .clr-flex-md-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .clr-flex-md-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .clr-flex-md-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .clr-flex-md-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .clr-justify-content-md-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .clr-justify-content-md-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .clr-justify-content-md-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .clr-justify-content-md-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .clr-justify-content-md-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .clr-align-items-md-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .clr-align-items-md-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .clr-align-items-md-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .clr-align-items-md-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .clr-align-items-md-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .clr-align-content-md-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .clr-align-content-md-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .clr-align-content-md-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .clr-align-content-md-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .clr-align-content-md-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .clr-align-content-md-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .clr-align-self-md-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .clr-align-self-md-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .clr-align-self-md-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .clr-align-self-md-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .clr-align-self-md-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .clr-align-self-md-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } } @media (min-width: 992px) { - .clr-flex-lg-row { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: row !important; - flex-direction: row !important; - } - .clr-flex-lg-column { - -webkit-box-orient: vertical !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: column !important; - flex-direction: column !important; - } - .clr-flex-lg-row-reverse { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: row-reverse !important; - flex-direction: row-reverse !important; - } - .clr-flex-lg-column-reverse { - -webkit-box-orient: vertical !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: column-reverse !important; - flex-direction: column-reverse !important; - } - .clr-flex-lg-wrap { - -ms-flex-wrap: wrap !important; - flex-wrap: wrap !important; - } - .clr-flex-lg-nowrap { - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - } - .clr-flex-lg-wrap-reverse { - -ms-flex-wrap: wrap-reverse !important; - flex-wrap: wrap-reverse !important; - } - .clr-flex-lg-fill { - -webkit-box-flex: 1 !important; - -ms-flex: 1 1 auto !important; - flex: 1 1 auto !important; - } - .clr-flex-lg-grow-0 { - -webkit-box-flex: 0 !important; - -ms-flex-positive: 0 !important; - flex-grow: 0 !important; - } - .clr-flex-lg-grow-1 { - -webkit-box-flex: 1 !important; - -ms-flex-positive: 1 !important; - flex-grow: 1 !important; - } - .clr-flex-lg-shrink-0 { - -ms-flex-negative: 0 !important; - flex-shrink: 0 !important; - } - .clr-flex-lg-shrink-1 { - -ms-flex-negative: 1 !important; - flex-shrink: 1 !important; - } - .clr-justify-content-lg-start { - -webkit-box-pack: start !important; - -ms-flex-pack: start !important; - justify-content: flex-start !important; - } - .clr-justify-content-lg-end { - -webkit-box-pack: end !important; - -ms-flex-pack: end !important; - justify-content: flex-end !important; - } - .clr-justify-content-lg-center { - -webkit-box-pack: center !important; - -ms-flex-pack: center !important; - justify-content: center !important; - } - .clr-justify-content-lg-between { - -webkit-box-pack: justify !important; - -ms-flex-pack: justify !important; - justify-content: space-between !important; - } - .clr-justify-content-lg-around { - -ms-flex-pack: distribute !important; - justify-content: space-around !important; - } - .clr-align-items-lg-start { - -webkit-box-align: start !important; - -ms-flex-align: start !important; - align-items: flex-start !important; - } - .clr-align-items-lg-end { - -webkit-box-align: end !important; - -ms-flex-align: end !important; - align-items: flex-end !important; - } - .clr-align-items-lg-center { - -webkit-box-align: center !important; - -ms-flex-align: center !important; - align-items: center !important; - } - .clr-align-items-lg-baseline { - -webkit-box-align: baseline !important; - -ms-flex-align: baseline !important; - align-items: baseline !important; - } - .clr-align-items-lg-stretch { - -webkit-box-align: stretch !important; - -ms-flex-align: stretch !important; - align-items: stretch !important; - } - .clr-align-content-lg-start { - -ms-flex-line-pack: start !important; - align-content: flex-start !important; - } - .clr-align-content-lg-end { - -ms-flex-line-pack: end !important; - align-content: flex-end !important; - } - .clr-align-content-lg-center { - -ms-flex-line-pack: center !important; - align-content: center !important; - } - .clr-align-content-lg-between { - -ms-flex-line-pack: justify !important; - align-content: space-between !important; - } - .clr-align-content-lg-around { - -ms-flex-line-pack: distribute !important; - align-content: space-around !important; - } - .clr-align-content-lg-stretch { - -ms-flex-line-pack: stretch !important; - align-content: stretch !important; - } - .clr-align-self-lg-auto { - -ms-flex-item-align: auto !important; - align-self: auto !important; - } - .clr-align-self-lg-start { - -ms-flex-item-align: start !important; - align-self: flex-start !important; - } - .clr-align-self-lg-end { - -ms-flex-item-align: end !important; - align-self: flex-end !important; - } - .clr-align-self-lg-center { - -ms-flex-item-align: center !important; - align-self: center !important; - } - .clr-align-self-lg-baseline { - -ms-flex-item-align: baseline !important; - align-self: baseline !important; - } - .clr-align-self-lg-stretch { - -ms-flex-item-align: stretch !important; - align-self: stretch !important; - } -} -@media (min-width: 1200px) { - .clr-flex-xl-row { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: row !important; - flex-direction: row !important; - } - .clr-flex-xl-column { - -webkit-box-orient: vertical !important; - -webkit-box-direction: normal !important; - -ms-flex-direction: column !important; - flex-direction: column !important; - } - .clr-flex-xl-row-reverse { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: row-reverse !important; - flex-direction: row-reverse !important; - } - .clr-flex-xl-column-reverse { - -webkit-box-orient: vertical !important; - -webkit-box-direction: reverse !important; - -ms-flex-direction: column-reverse !important; - flex-direction: column-reverse !important; - } - .clr-flex-xl-wrap { - -ms-flex-wrap: wrap !important; - flex-wrap: wrap !important; - } - .clr-flex-xl-nowrap { - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - } - .clr-flex-xl-wrap-reverse { - -ms-flex-wrap: wrap-reverse !important; - flex-wrap: wrap-reverse !important; - } - .clr-flex-xl-fill { - -webkit-box-flex: 1 !important; - -ms-flex: 1 1 auto !important; - flex: 1 1 auto !important; - } - .clr-flex-xl-grow-0 { - -webkit-box-flex: 0 !important; - -ms-flex-positive: 0 !important; - flex-grow: 0 !important; - } - .clr-flex-xl-grow-1 { - -webkit-box-flex: 1 !important; - -ms-flex-positive: 1 !important; - flex-grow: 1 !important; - } - .clr-flex-xl-shrink-0 { - -ms-flex-negative: 0 !important; - flex-shrink: 0 !important; - } - .clr-flex-xl-shrink-1 { - -ms-flex-negative: 1 !important; - flex-shrink: 1 !important; - } - .clr-justify-content-xl-start { - -webkit-box-pack: start !important; - -ms-flex-pack: start !important; - justify-content: flex-start !important; - } - .clr-justify-content-xl-end { - -webkit-box-pack: end !important; - -ms-flex-pack: end !important; - justify-content: flex-end !important; - } - .clr-justify-content-xl-center { - -webkit-box-pack: center !important; - -ms-flex-pack: center !important; - justify-content: center !important; - } - .clr-justify-content-xl-between { - -webkit-box-pack: justify !important; - -ms-flex-pack: justify !important; - justify-content: space-between !important; - } - .clr-justify-content-xl-around { - -ms-flex-pack: distribute !important; - justify-content: space-around !important; - } - .clr-align-items-xl-start { - -webkit-box-align: start !important; - -ms-flex-align: start !important; - align-items: flex-start !important; - } - .clr-align-items-xl-end { - -webkit-box-align: end !important; - -ms-flex-align: end !important; - align-items: flex-end !important; - } - .clr-align-items-xl-center { - -webkit-box-align: center !important; - -ms-flex-align: center !important; - align-items: center !important; - } - .clr-align-items-xl-baseline { - -webkit-box-align: baseline !important; - -ms-flex-align: baseline !important; - align-items: baseline !important; - } - .clr-align-items-xl-stretch { - -webkit-box-align: stretch !important; - -ms-flex-align: stretch !important; - align-items: stretch !important; - } - .clr-align-content-xl-start { - -ms-flex-line-pack: start !important; - align-content: flex-start !important; - } - .clr-align-content-xl-end { - -ms-flex-line-pack: end !important; - align-content: flex-end !important; - } - .clr-align-content-xl-center { - -ms-flex-line-pack: center !important; - align-content: center !important; - } - .clr-align-content-xl-between { - -ms-flex-line-pack: justify !important; - align-content: space-between !important; - } - .clr-align-content-xl-around { - -ms-flex-line-pack: distribute !important; - align-content: space-around !important; - } - .clr-align-content-xl-stretch { - -ms-flex-line-pack: stretch !important; - align-content: stretch !important; - } - .clr-align-self-xl-auto { - -ms-flex-item-align: auto !important; - align-self: auto !important; - } - .clr-align-self-xl-start { - -ms-flex-item-align: start !important; - align-self: flex-start !important; - } - .clr-align-self-xl-end { - -ms-flex-item-align: end !important; - align-self: flex-end !important; - } - .clr-align-self-xl-center { - -ms-flex-item-align: center !important; - align-self: center !important; - } - .clr-align-self-xl-baseline { - -ms-flex-item-align: baseline !important; - align-self: baseline !important; - } - .clr-align-self-xl-stretch { - -ms-flex-item-align: stretch !important; - align-self: stretch !important; - } -} -.clr-flex-xs-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; -} -.clr-flex-xs-last { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; -} + .clr-flex-lg-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .clr-flex-lg-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .clr-flex-lg-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .clr-flex-lg-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .clr-flex-lg-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .clr-flex-lg-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .clr-flex-lg-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .clr-flex-lg-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .clr-flex-lg-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .clr-flex-lg-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .clr-flex-lg-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .clr-flex-lg-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .clr-justify-content-lg-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .clr-justify-content-lg-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .clr-justify-content-lg-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .clr-justify-content-lg-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .clr-justify-content-lg-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .clr-align-items-lg-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .clr-align-items-lg-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .clr-align-items-lg-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .clr-align-items-lg-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .clr-align-items-lg-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .clr-align-content-lg-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .clr-align-content-lg-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .clr-align-content-lg-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .clr-align-content-lg-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .clr-align-content-lg-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .clr-align-content-lg-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .clr-align-self-lg-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .clr-align-self-lg-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .clr-align-self-lg-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .clr-align-self-lg-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .clr-align-self-lg-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .clr-align-self-lg-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} +@media (min-width: 1200px) { + .clr-flex-xl-row { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: row !important; + flex-direction: row !important; + } + .clr-flex-xl-column { + -webkit-box-orient: vertical !important; + -webkit-box-direction: normal !important; + -ms-flex-direction: column !important; + flex-direction: column !important; + } + .clr-flex-xl-row-reverse { + -webkit-box-orient: horizontal !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: row-reverse !important; + flex-direction: row-reverse !important; + } + .clr-flex-xl-column-reverse { + -webkit-box-orient: vertical !important; + -webkit-box-direction: reverse !important; + -ms-flex-direction: column-reverse !important; + flex-direction: column-reverse !important; + } + .clr-flex-xl-wrap { + -ms-flex-wrap: wrap !important; + flex-wrap: wrap !important; + } + .clr-flex-xl-nowrap { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } + .clr-flex-xl-wrap-reverse { + -ms-flex-wrap: wrap-reverse !important; + flex-wrap: wrap-reverse !important; + } + .clr-flex-xl-fill { + -webkit-box-flex: 1 !important; + -ms-flex: 1 1 auto !important; + flex: 1 1 auto !important; + } + .clr-flex-xl-grow-0 { + -webkit-box-flex: 0 !important; + -ms-flex-positive: 0 !important; + flex-grow: 0 !important; + } + .clr-flex-xl-grow-1 { + -webkit-box-flex: 1 !important; + -ms-flex-positive: 1 !important; + flex-grow: 1 !important; + } + .clr-flex-xl-shrink-0 { + -ms-flex-negative: 0 !important; + flex-shrink: 0 !important; + } + .clr-flex-xl-shrink-1 { + -ms-flex-negative: 1 !important; + flex-shrink: 1 !important; + } + .clr-justify-content-xl-start { + -webkit-box-pack: start !important; + -ms-flex-pack: start !important; + justify-content: flex-start !important; + } + .clr-justify-content-xl-end { + -webkit-box-pack: end !important; + -ms-flex-pack: end !important; + justify-content: flex-end !important; + } + .clr-justify-content-xl-center { + -webkit-box-pack: center !important; + -ms-flex-pack: center !important; + justify-content: center !important; + } + .clr-justify-content-xl-between { + -webkit-box-pack: justify !important; + -ms-flex-pack: justify !important; + justify-content: space-between !important; + } + .clr-justify-content-xl-around { + -ms-flex-pack: distribute !important; + justify-content: space-around !important; + } + .clr-align-items-xl-start { + -webkit-box-align: start !important; + -ms-flex-align: start !important; + align-items: flex-start !important; + } + .clr-align-items-xl-end { + -webkit-box-align: end !important; + -ms-flex-align: end !important; + align-items: flex-end !important; + } + .clr-align-items-xl-center { + -webkit-box-align: center !important; + -ms-flex-align: center !important; + align-items: center !important; + } + .clr-align-items-xl-baseline { + -webkit-box-align: baseline !important; + -ms-flex-align: baseline !important; + align-items: baseline !important; + } + .clr-align-items-xl-stretch { + -webkit-box-align: stretch !important; + -ms-flex-align: stretch !important; + align-items: stretch !important; + } + .clr-align-content-xl-start { + -ms-flex-line-pack: start !important; + align-content: flex-start !important; + } + .clr-align-content-xl-end { + -ms-flex-line-pack: end !important; + align-content: flex-end !important; + } + .clr-align-content-xl-center { + -ms-flex-line-pack: center !important; + align-content: center !important; + } + .clr-align-content-xl-between { + -ms-flex-line-pack: justify !important; + align-content: space-between !important; + } + .clr-align-content-xl-around { + -ms-flex-line-pack: distribute !important; + align-content: space-around !important; + } + .clr-align-content-xl-stretch { + -ms-flex-line-pack: stretch !important; + align-content: stretch !important; + } + .clr-align-self-xl-auto { + -ms-flex-item-align: auto !important; + align-self: auto !important; + } + .clr-align-self-xl-start { + -ms-flex-item-align: start !important; + align-self: flex-start !important; + } + .clr-align-self-xl-end { + -ms-flex-item-align: end !important; + align-self: flex-end !important; + } + .clr-align-self-xl-center { + -ms-flex-item-align: center !important; + align-self: center !important; + } + .clr-align-self-xl-baseline { + -ms-flex-item-align: baseline !important; + align-self: baseline !important; + } + .clr-align-self-xl-stretch { + -ms-flex-item-align: stretch !important; + align-self: stretch !important; + } +} +.clr-flex-xs-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; +} +.clr-flex-xs-last { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; +} .clr-flex-xs-unordered { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; +} +.clr-flex-items-xs-top { + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; +} +.clr-flex-items-xs-middle { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} +.clr-flex-items-xs-bottom { + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; +} +.clr-flex-xs-top { + -ms-flex-item-align: start; + align-self: flex-start; +} +.clr-flex-xs-middle { + -ms-flex-item-align: center; + align-self: center; +} +.clr-flex-xs-bottom { + -ms-flex-item-align: end; + align-self: flex-end; +} +.clr-flex-items-xs-left { + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; +} +.clr-flex-items-xs-center { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} +.clr-flex-items-xs-right { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; +} +.clr-flex-items-xs-around { + -ms-flex-pack: distribute; + justify-content: space-around; +} +.clr-flex-items-xs-between { + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; +} +@media (min-width: 576px) { + .clr-flex-sm-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-flex-sm-last { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-flex-sm-unordered { -webkit-box-ordinal-group: 1; -ms-flex-order: 0; order: 0; + } } -.clr-flex-items-xs-top { +@media (min-width: 576px) { + .clr-flex-items-sm-top { -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; -} -.clr-flex-items-xs-middle { + } + .clr-flex-items-sm-middle { -webkit-box-align: center; -ms-flex-align: center; align-items: center; -} -.clr-flex-items-xs-bottom { + } + .clr-flex-items-sm-bottom { -webkit-box-align: end; -ms-flex-align: end; align-items: flex-end; + } } -.clr-flex-xs-top { +@media (min-width: 576px) { + .clr-flex-sm-top { -ms-flex-item-align: start; align-self: flex-start; -} -.clr-flex-xs-middle { + } + .clr-flex-sm-middle { -ms-flex-item-align: center; align-self: center; -} -.clr-flex-xs-bottom { + } + .clr-flex-sm-bottom { -ms-flex-item-align: end; align-self: flex-end; + } } -.clr-flex-items-xs-left { +@media (min-width: 576px) { + .clr-flex-items-sm-left { -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; -} -.clr-flex-items-xs-center { + } + .clr-flex-items-sm-center { -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -} -.clr-flex-items-xs-right { + } + .clr-flex-items-sm-right { -webkit-box-pack: end; -ms-flex-pack: end; justify-content: flex-end; -} -.clr-flex-items-xs-around { + } + .clr-flex-items-sm-around { -ms-flex-pack: distribute; justify-content: space-around; -} -.clr-flex-items-xs-between { + } + .clr-flex-items-sm-between { -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; -} -@media (min-width: 576px) { - .clr-flex-sm-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-flex-sm-last { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-flex-sm-unordered { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } -} -@media (min-width: 576px) { - .clr-flex-items-sm-top { - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; - } - .clr-flex-items-sm-middle { - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - .clr-flex-items-sm-bottom { - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; - } -} -@media (min-width: 576px) { - .clr-flex-sm-top { - -ms-flex-item-align: start; - align-self: flex-start; - } - .clr-flex-sm-middle { - -ms-flex-item-align: center; - align-self: center; - } - .clr-flex-sm-bottom { - -ms-flex-item-align: end; - align-self: flex-end; - } -} -@media (min-width: 576px) { - .clr-flex-items-sm-left { - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - } - .clr-flex-items-sm-center { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - } - .clr-flex-items-sm-right { - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - } - .clr-flex-items-sm-around { - -ms-flex-pack: distribute; - justify-content: space-around; - } - .clr-flex-items-sm-between { - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - } -} -@media (min-width: 768px) { - .clr-flex-md-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-flex-md-last { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-flex-md-unordered { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } -} -@media (min-width: 768px) { - .clr-flex-items-md-top { - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; - } - .clr-flex-items-md-middle { - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - .clr-flex-items-md-bottom { - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; - } + } } @media (min-width: 768px) { - .clr-flex-md-top { - -ms-flex-item-align: start; - align-self: flex-start; - } - .clr-flex-md-middle { - -ms-flex-item-align: center; - align-self: center; - } - .clr-flex-md-bottom { - -ms-flex-item-align: end; - align-self: flex-end; - } + .clr-flex-md-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-flex-md-last { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-flex-md-unordered { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } } @media (min-width: 768px) { - .clr-flex-items-md-left { - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - } - .clr-flex-items-md-center { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - } - .clr-flex-items-md-right { - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - } - .clr-flex-items-md-around { - -ms-flex-pack: distribute; - justify-content: space-around; - } - .clr-flex-items-md-between { - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - } + .clr-flex-items-md-top { + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + } + .clr-flex-items-md-middle { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + .clr-flex-items-md-bottom { + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; + } +} +@media (min-width: 768px) { + .clr-flex-md-top { + -ms-flex-item-align: start; + align-self: flex-start; + } + .clr-flex-md-middle { + -ms-flex-item-align: center; + align-self: center; + } + .clr-flex-md-bottom { + -ms-flex-item-align: end; + align-self: flex-end; + } +} +@media (min-width: 768px) { + .clr-flex-items-md-left { + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .clr-flex-items-md-center { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + } + .clr-flex-items-md-right { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + } + .clr-flex-items-md-around { + -ms-flex-pack: distribute; + justify-content: space-around; + } + .clr-flex-items-md-between { + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + } } @media (min-width: 992px) { - .clr-flex-lg-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-flex-lg-last { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-flex-lg-unordered { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } + .clr-flex-lg-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-flex-lg-last { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-flex-lg-unordered { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } } @media (min-width: 992px) { - .clr-flex-items-lg-top { - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; - } - .clr-flex-items-lg-middle { - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - .clr-flex-items-lg-bottom { - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; - } + .clr-flex-items-lg-top { + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + } + .clr-flex-items-lg-middle { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + .clr-flex-items-lg-bottom { + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; + } } @media (min-width: 992px) { - .clr-flex-lg-top { - -ms-flex-item-align: start; - align-self: flex-start; - } - .clr-flex-lg-middle { - -ms-flex-item-align: center; - align-self: center; - } - .clr-flex-lg-bottom { - -ms-flex-item-align: end; - align-self: flex-end; - } + .clr-flex-lg-top { + -ms-flex-item-align: start; + align-self: flex-start; + } + .clr-flex-lg-middle { + -ms-flex-item-align: center; + align-self: center; + } + .clr-flex-lg-bottom { + -ms-flex-item-align: end; + align-self: flex-end; + } } @media (min-width: 992px) { - .clr-flex-items-lg-left { - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - } - .clr-flex-items-lg-center { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - } - .clr-flex-items-lg-right { - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - } - .clr-flex-items-lg-around { - -ms-flex-pack: distribute; - justify-content: space-around; - } - .clr-flex-items-lg-between { - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - } + .clr-flex-items-lg-left { + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .clr-flex-items-lg-center { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + } + .clr-flex-items-lg-right { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + } + .clr-flex-items-lg-around { + -ms-flex-pack: distribute; + justify-content: space-around; + } + .clr-flex-items-lg-between { + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + } } @media (min-width: 1200px) { - .clr-flex-xl-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-flex-xl-last { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-flex-xl-unordered { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } + .clr-flex-xl-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-flex-xl-last { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-flex-xl-unordered { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } } @media (min-width: 1200px) { - .clr-flex-items-xl-top { - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; - } - .clr-flex-items-xl-middle { - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - .clr-flex-items-xl-bottom { - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; - } + .clr-flex-items-xl-top { + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + } + .clr-flex-items-xl-middle { + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + .clr-flex-items-xl-bottom { + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; + } } @media (min-width: 1200px) { - .clr-flex-xl-top { - -ms-flex-item-align: start; - align-self: flex-start; - } - .clr-flex-xl-middle { - -ms-flex-item-align: center; - align-self: center; - } - .clr-flex-xl-bottom { - -ms-flex-item-align: end; - align-self: flex-end; - } + .clr-flex-xl-top { + -ms-flex-item-align: start; + align-self: flex-start; + } + .clr-flex-xl-middle { + -ms-flex-item-align: center; + align-self: center; + } + .clr-flex-xl-bottom { + -ms-flex-item-align: end; + align-self: flex-end; + } } @media (min-width: 1200px) { - .clr-flex-items-xl-left { - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - } - .clr-flex-items-xl-center { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - } - .clr-flex-items-xl-right { - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - } - .clr-flex-items-xl-around { - -ms-flex-pack: distribute; - justify-content: space-around; - } - .clr-flex-items-xl-between { - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - } + .clr-flex-items-xl-left { + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + } + .clr-flex-items-xl-center { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + } + .clr-flex-items-xl-right { + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + } + .clr-flex-items-xl-around { + -ms-flex-pack: distribute; + justify-content: space-around; + } + .clr-flex-items-xl-between { + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + } } .clr-float-xs-left { - float: left !important; + float: left !important; } .clr-float-xs-right { - float: right !important; + float: right !important; } .clr-float-xs-none { - float: none !important; + float: none !important; } @media (min-width: 576px) { - .clr-float-sm-left { - float: left !important; - } - .clr-float-sm-right { - float: right !important; - } - .clr-float-sm-none { - float: none !important; - } + .clr-float-sm-left { + float: left !important; + } + .clr-float-sm-right { + float: right !important; + } + .clr-float-sm-none { + float: none !important; + } } @media (min-width: 768px) { - .clr-float-md-left { - float: left !important; - } - .clr-float-md-right { - float: right !important; - } - .clr-float-md-none { - float: none !important; - } + .clr-float-md-left { + float: left !important; + } + .clr-float-md-right { + float: right !important; + } + .clr-float-md-none { + float: none !important; + } } @media (min-width: 992px) { - .clr-float-lg-left { - float: left !important; - } - .clr-float-lg-right { - float: right !important; - } - .clr-float-lg-none { - float: none !important; - } + .clr-float-lg-left { + float: left !important; + } + .clr-float-lg-right { + float: right !important; + } + .clr-float-lg-none { + float: none !important; + } } @media (min-width: 1200px) { - .clr-float-xl-left { - float: left !important; - } - .clr-float-xl-right { - float: right !important; - } - .clr-float-xl-none { - float: none !important; - } + .clr-float-xl-left { + float: left !important; + } + .clr-float-xl-right { + float: right !important; + } + .clr-float-xl-none { + float: none !important; + } } .clr-invisible { - visibility: hidden !important; + visibility: hidden !important; } .clr-hidden-xs-up { - display: none !important; + display: none !important; } @media (max-width: 575.98px) { - .clr-hidden-xs-down { - display: none !important; - } + .clr-hidden-xs-down { + display: none !important; + } } @media (min-width: 576px) { - .clr-hidden-sm-up { - display: none !important; - } + .clr-hidden-sm-up { + display: none !important; + } } @media (max-width: 767.98px) { - .clr-hidden-sm-down { - display: none !important; - } + .clr-hidden-sm-down { + display: none !important; + } } @media (min-width: 768px) { - .clr-hidden-md-up { - display: none !important; - } + .clr-hidden-md-up { + display: none !important; + } } @media (max-width: 991.98px) { - .clr-hidden-md-down { - display: none !important; - } + .clr-hidden-md-down { + display: none !important; + } } @media (min-width: 992px) { - .clr-hidden-lg-up { - display: none !important; - } + .clr-hidden-lg-up { + display: none !important; + } } @media (max-width: 1199.98px) { - .clr-hidden-lg-down { - display: none !important; - } + .clr-hidden-lg-down { + display: none !important; + } } @media (min-width: 1200px) { - .clr-hidden-xl-up { - display: none !important; - } + .clr-hidden-xl-up { + display: none !important; + } } .clr-hidden-xl-down { - display: none !important; + display: none !important; } .clr-visible-print-block { - display: none !important; + display: none !important; } @media print { - .clr-visible-print-block { - display: block !important; - } + .clr-visible-print-block { + display: block !important; + } } .clr-visible-print-inline { - display: none !important; + display: none !important; } @media print { - .clr-visible-print-inline { - display: inline !important; - } + .clr-visible-print-inline { + display: inline !important; + } } .clr-visible-print-inline-block { - display: none !important; + display: none !important; } @media print { - .clr-visible-print-inline-block { - display: inline-block !important; - } + .clr-visible-print-inline-block { + display: inline-block !important; + } } @media print { - .clr-hidden-print { - display: none !important; - } + .clr-hidden-print { + display: none !important; + } } .clr-row { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - margin-right: -0.6rem; - margin-left: -0.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -0.6rem; + margin-left: -0.6rem; } .clr-no-gutters { - margin-right: 0; + margin-right: 0; + margin-left: 0; +} +.clr-no-gutters > .clr-col, +.clr-no-gutters > [class*="clr-col-"] { + padding-right: 0; + padding-left: 0; +} +.clr-col, +.clr-col-1, +.clr-col-10, +.clr-col-11, +.clr-col-12, +.clr-col-2, +.clr-col-3, +.clr-col-4, +.clr-col-5, +.clr-col-6, +.clr-col-7, +.clr-col-8, +.clr-col-9, +.clr-col-auto, +.clr-col-lg, +.clr-col-lg-1, +.clr-col-lg-10, +.clr-col-lg-11, +.clr-col-lg-12, +.clr-col-lg-2, +.clr-col-lg-3, +.clr-col-lg-4, +.clr-col-lg-5, +.clr-col-lg-6, +.clr-col-lg-7, +.clr-col-lg-8, +.clr-col-lg-9, +.clr-col-lg-auto, +.clr-col-md, +.clr-col-md-1, +.clr-col-md-10, +.clr-col-md-11, +.clr-col-md-12, +.clr-col-md-2, +.clr-col-md-3, +.clr-col-md-4, +.clr-col-md-5, +.clr-col-md-6, +.clr-col-md-7, +.clr-col-md-8, +.clr-col-md-9, +.clr-col-md-auto, +.clr-col-sm, +.clr-col-sm-1, +.clr-col-sm-10, +.clr-col-sm-11, +.clr-col-sm-12, +.clr-col-sm-2, +.clr-col-sm-3, +.clr-col-sm-4, +.clr-col-sm-5, +.clr-col-sm-6, +.clr-col-sm-7, +.clr-col-sm-8, +.clr-col-sm-9, +.clr-col-sm-auto, +.clr-col-xl, +.clr-col-xl-1, +.clr-col-xl-10, +.clr-col-xl-11, +.clr-col-xl-12, +.clr-col-xl-2, +.clr-col-xl-3, +.clr-col-xl-4, +.clr-col-xl-5, +.clr-col-xl-6, +.clr-col-xl-7, +.clr-col-xl-8, +.clr-col-xl-9, +.clr-col-xl-auto { + width: 100%; + min-height: 0.05rem; + padding-right: 0.6rem; + padding-left: 0.6rem; +} +.clr-col { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; +} +.clr-col-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; +} +.clr-col-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; +} +.clr-col-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; +} +.clr-col-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} +.clr-col-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; +} +.clr-col-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; +} +.clr-col-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} +.clr-col-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; +} +.clr-col-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; +} +.clr-col-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} +.clr-col-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; +} +.clr-col-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; +} +.clr-col-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} +.clr-order-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; +} +.clr-order-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; +} +.clr-order-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; +} +.clr-order-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; +} +.clr-order-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; +} +.clr-order-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; +} +.clr-order-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; +} +.clr-order-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; +} +.clr-order-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; +} +.clr-order-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; +} +.clr-order-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; +} +.clr-order-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; +} +.clr-order-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; +} +.clr-order-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; +} +.clr-order-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; +} +.clr-offset-1 { + margin-left: 8.3333333333%; +} +.clr-offset-2 { + margin-left: 16.6666666667%; +} +.clr-offset-3 { + margin-left: 25%; +} +.clr-offset-4 { + margin-left: 33.3333333333%; +} +.clr-offset-5 { + margin-left: 41.6666666667%; +} +.clr-offset-6 { + margin-left: 50%; +} +.clr-offset-7 { + margin-left: 58.3333333333%; +} +.clr-offset-8 { + margin-left: 66.6666666667%; +} +.clr-offset-9 { + margin-left: 75%; +} +.clr-offset-10 { + margin-left: 83.3333333333%; +} +.clr-offset-11 { + margin-left: 91.6666666667%; +} +@media (min-width: 576px) { + .clr-col-sm { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .clr-col-sm-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .clr-col-sm-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .clr-col-sm-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .clr-col-sm-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .clr-col-sm-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .clr-col-sm-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .clr-col-sm-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .clr-col-sm-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .clr-col-sm-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .clr-col-sm-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .clr-col-sm-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .clr-col-sm-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .clr-col-sm-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .clr-order-sm-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-order-sm-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .clr-order-sm-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .clr-order-sm-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-order-sm-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .clr-order-sm-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .clr-order-sm-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .clr-order-sm-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .clr-order-sm-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .clr-order-sm-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .clr-order-sm-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .clr-order-sm-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .clr-order-sm-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .clr-order-sm-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .clr-order-sm-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .clr-offset-sm-0 { + margin-left: 0; + } + .clr-offset-sm-1 { + margin-left: 8.3333333333%; + } + .clr-offset-sm-2 { + margin-left: 16.6666666667%; + } + .clr-offset-sm-3 { + margin-left: 25%; + } + .clr-offset-sm-4 { + margin-left: 33.3333333333%; + } + .clr-offset-sm-5 { + margin-left: 41.6666666667%; + } + .clr-offset-sm-6 { + margin-left: 50%; + } + .clr-offset-sm-7 { + margin-left: 58.3333333333%; + } + .clr-offset-sm-8 { + margin-left: 66.6666666667%; + } + .clr-offset-sm-9 { + margin-left: 75%; + } + .clr-offset-sm-10 { + margin-left: 83.3333333333%; + } + .clr-offset-sm-11 { + margin-left: 91.6666666667%; + } +} +@media (min-width: 768px) { + .clr-col-md { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .clr-col-md-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .clr-col-md-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .clr-col-md-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .clr-col-md-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .clr-col-md-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .clr-col-md-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .clr-col-md-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .clr-col-md-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .clr-col-md-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .clr-col-md-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .clr-col-md-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .clr-col-md-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .clr-col-md-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .clr-order-md-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-order-md-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .clr-order-md-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .clr-order-md-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-order-md-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .clr-order-md-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .clr-order-md-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .clr-order-md-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .clr-order-md-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .clr-order-md-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .clr-order-md-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .clr-order-md-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .clr-order-md-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .clr-order-md-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .clr-order-md-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .clr-offset-md-0 { margin-left: 0; + } + .clr-offset-md-1 { + margin-left: 8.3333333333%; + } + .clr-offset-md-2 { + margin-left: 16.6666666667%; + } + .clr-offset-md-3 { + margin-left: 25%; + } + .clr-offset-md-4 { + margin-left: 33.3333333333%; + } + .clr-offset-md-5 { + margin-left: 41.6666666667%; + } + .clr-offset-md-6 { + margin-left: 50%; + } + .clr-offset-md-7 { + margin-left: 58.3333333333%; + } + .clr-offset-md-8 { + margin-left: 66.6666666667%; + } + .clr-offset-md-9 { + margin-left: 75%; + } + .clr-offset-md-10 { + margin-left: 83.3333333333%; + } + .clr-offset-md-11 { + margin-left: 91.6666666667%; + } } -.clr-no-gutters > .clr-col, -.clr-no-gutters > [class*='clr-col-'] { - padding-right: 0; - padding-left: 0; -} -.clr-col, -.clr-col-1, -.clr-col-10, -.clr-col-11, -.clr-col-12, -.clr-col-2, -.clr-col-3, -.clr-col-4, -.clr-col-5, -.clr-col-6, -.clr-col-7, -.clr-col-8, -.clr-col-9, -.clr-col-auto, -.clr-col-lg, -.clr-col-lg-1, -.clr-col-lg-10, -.clr-col-lg-11, -.clr-col-lg-12, -.clr-col-lg-2, -.clr-col-lg-3, -.clr-col-lg-4, -.clr-col-lg-5, -.clr-col-lg-6, -.clr-col-lg-7, -.clr-col-lg-8, -.clr-col-lg-9, -.clr-col-lg-auto, -.clr-col-md, -.clr-col-md-1, -.clr-col-md-10, -.clr-col-md-11, -.clr-col-md-12, -.clr-col-md-2, -.clr-col-md-3, -.clr-col-md-4, -.clr-col-md-5, -.clr-col-md-6, -.clr-col-md-7, -.clr-col-md-8, -.clr-col-md-9, -.clr-col-md-auto, -.clr-col-sm, -.clr-col-sm-1, -.clr-col-sm-10, -.clr-col-sm-11, -.clr-col-sm-12, -.clr-col-sm-2, -.clr-col-sm-3, -.clr-col-sm-4, -.clr-col-sm-5, -.clr-col-sm-6, -.clr-col-sm-7, -.clr-col-sm-8, -.clr-col-sm-9, -.clr-col-sm-auto, -.clr-col-xl, -.clr-col-xl-1, -.clr-col-xl-10, -.clr-col-xl-11, -.clr-col-xl-12, -.clr-col-xl-2, -.clr-col-xl-3, -.clr-col-xl-4, -.clr-col-xl-5, -.clr-col-xl-6, -.clr-col-xl-7, -.clr-col-xl-8, -.clr-col-xl-9, -.clr-col-xl-auto { - width: 100%; - min-height: 0.05rem; - padding-right: 0.6rem; - padding-left: 0.6rem; -} -.clr-col { +@media (min-width: 992px) { + .clr-col-lg { -ms-flex-preferred-size: 0; flex-basis: 0; -webkit-box-flex: 1; -ms-flex-positive: 1; flex-grow: 1; max-width: 100%; -} -.clr-col-auto { + } + .clr-col-lg-auto { -webkit-box-flex: 0; -ms-flex: 0 0 auto; flex: 0 0 auto; width: auto; max-width: none; -} -.clr-col-1 { + } + .clr-col-lg-1 { -webkit-box-flex: 0; -ms-flex: 0 0 8.3333333333%; flex: 0 0 8.3333333333%; max-width: 8.3333333333%; -} -.clr-col-2 { + } + .clr-col-lg-2 { -webkit-box-flex: 0; -ms-flex: 0 0 16.6666666667%; flex: 0 0 16.6666666667%; max-width: 16.6666666667%; -} -.clr-col-3 { + } + .clr-col-lg-3 { -webkit-box-flex: 0; -ms-flex: 0 0 25%; flex: 0 0 25%; max-width: 25%; -} -.clr-col-4 { + } + .clr-col-lg-4 { -webkit-box-flex: 0; -ms-flex: 0 0 33.3333333333%; flex: 0 0 33.3333333333%; max-width: 33.3333333333%; -} -.clr-col-5 { + } + .clr-col-lg-5 { -webkit-box-flex: 0; -ms-flex: 0 0 41.6666666667%; flex: 0 0 41.6666666667%; max-width: 41.6666666667%; -} -.clr-col-6 { + } + .clr-col-lg-6 { -webkit-box-flex: 0; -ms-flex: 0 0 50%; flex: 0 0 50%; max-width: 50%; -} -.clr-col-7 { + } + .clr-col-lg-7 { -webkit-box-flex: 0; -ms-flex: 0 0 58.3333333333%; flex: 0 0 58.3333333333%; max-width: 58.3333333333%; -} -.clr-col-8 { + } + .clr-col-lg-8 { -webkit-box-flex: 0; -ms-flex: 0 0 66.6666666667%; flex: 0 0 66.6666666667%; max-width: 66.6666666667%; -} -.clr-col-9 { + } + .clr-col-lg-9 { -webkit-box-flex: 0; -ms-flex: 0 0 75%; flex: 0 0 75%; max-width: 75%; -} -.clr-col-10 { + } + .clr-col-lg-10 { -webkit-box-flex: 0; -ms-flex: 0 0 83.3333333333%; flex: 0 0 83.3333333333%; max-width: 83.3333333333%; -} -.clr-col-11 { + } + .clr-col-lg-11 { -webkit-box-flex: 0; -ms-flex: 0 0 91.6666666667%; flex: 0 0 91.6666666667%; max-width: 91.6666666667%; -} -.clr-col-12 { + } + .clr-col-lg-12 { -webkit-box-flex: 0; -ms-flex: 0 0 100%; flex: 0 0 100%; max-width: 100%; -} -.clr-order-first { + } + .clr-order-lg-first { -webkit-box-ordinal-group: 0; -ms-flex-order: -1; order: -1; -} -.clr-order-last { + } + .clr-order-lg-last { -webkit-box-ordinal-group: 14; -ms-flex-order: 13; order: 13; -} -.clr-order-0 { + } + .clr-order-lg-0 { -webkit-box-ordinal-group: 1; -ms-flex-order: 0; order: 0; -} -.clr-order-1 { + } + .clr-order-lg-1 { -webkit-box-ordinal-group: 2; -ms-flex-order: 1; order: 1; -} -.clr-order-2 { + } + .clr-order-lg-2 { -webkit-box-ordinal-group: 3; -ms-flex-order: 2; order: 2; -} -.clr-order-3 { + } + .clr-order-lg-3 { -webkit-box-ordinal-group: 4; -ms-flex-order: 3; order: 3; + } + .clr-order-lg-4 { + -webkit-box-ordinal-group: 5; + -ms-flex-order: 4; + order: 4; + } + .clr-order-lg-5 { + -webkit-box-ordinal-group: 6; + -ms-flex-order: 5; + order: 5; + } + .clr-order-lg-6 { + -webkit-box-ordinal-group: 7; + -ms-flex-order: 6; + order: 6; + } + .clr-order-lg-7 { + -webkit-box-ordinal-group: 8; + -ms-flex-order: 7; + order: 7; + } + .clr-order-lg-8 { + -webkit-box-ordinal-group: 9; + -ms-flex-order: 8; + order: 8; + } + .clr-order-lg-9 { + -webkit-box-ordinal-group: 10; + -ms-flex-order: 9; + order: 9; + } + .clr-order-lg-10 { + -webkit-box-ordinal-group: 11; + -ms-flex-order: 10; + order: 10; + } + .clr-order-lg-11 { + -webkit-box-ordinal-group: 12; + -ms-flex-order: 11; + order: 11; + } + .clr-order-lg-12 { + -webkit-box-ordinal-group: 13; + -ms-flex-order: 12; + order: 12; + } + .clr-offset-lg-0 { + margin-left: 0; + } + .clr-offset-lg-1 { + margin-left: 8.3333333333%; + } + .clr-offset-lg-2 { + margin-left: 16.6666666667%; + } + .clr-offset-lg-3 { + margin-left: 25%; + } + .clr-offset-lg-4 { + margin-left: 33.3333333333%; + } + .clr-offset-lg-5 { + margin-left: 41.6666666667%; + } + .clr-offset-lg-6 { + margin-left: 50%; + } + .clr-offset-lg-7 { + margin-left: 58.3333333333%; + } + .clr-offset-lg-8 { + margin-left: 66.6666666667%; + } + .clr-offset-lg-9 { + margin-left: 75%; + } + .clr-offset-lg-10 { + margin-left: 83.3333333333%; + } + .clr-offset-lg-11 { + margin-left: 91.6666666667%; + } } -.clr-order-4 { +@media (min-width: 1200px) { + .clr-col-xl { + -ms-flex-preferred-size: 0; + flex-basis: 0; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + max-width: 100%; + } + .clr-col-xl-auto { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: auto; + max-width: none; + } + .clr-col-xl-1 { + -webkit-box-flex: 0; + -ms-flex: 0 0 8.3333333333%; + flex: 0 0 8.3333333333%; + max-width: 8.3333333333%; + } + .clr-col-xl-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.6666666667%; + flex: 0 0 16.6666666667%; + max-width: 16.6666666667%; + } + .clr-col-xl-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } + .clr-col-xl-4 { + -webkit-box-flex: 0; + -ms-flex: 0 0 33.3333333333%; + flex: 0 0 33.3333333333%; + max-width: 33.3333333333%; + } + .clr-col-xl-5 { + -webkit-box-flex: 0; + -ms-flex: 0 0 41.6666666667%; + flex: 0 0 41.6666666667%; + max-width: 41.6666666667%; + } + .clr-col-xl-6 { + -webkit-box-flex: 0; + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } + .clr-col-xl-7 { + -webkit-box-flex: 0; + -ms-flex: 0 0 58.3333333333%; + flex: 0 0 58.3333333333%; + max-width: 58.3333333333%; + } + .clr-col-xl-8 { + -webkit-box-flex: 0; + -ms-flex: 0 0 66.6666666667%; + flex: 0 0 66.6666666667%; + max-width: 66.6666666667%; + } + .clr-col-xl-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } + .clr-col-xl-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.3333333333%; + flex: 0 0 83.3333333333%; + max-width: 83.3333333333%; + } + .clr-col-xl-11 { + -webkit-box-flex: 0; + -ms-flex: 0 0 91.6666666667%; + flex: 0 0 91.6666666667%; + max-width: 91.6666666667%; + } + .clr-col-xl-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; + } + .clr-order-xl-first { + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + } + .clr-order-xl-last { + -webkit-box-ordinal-group: 14; + -ms-flex-order: 13; + order: 13; + } + .clr-order-xl-0 { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; + } + .clr-order-xl-1 { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + } + .clr-order-xl-2 { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + } + .clr-order-xl-3 { + -webkit-box-ordinal-group: 4; + -ms-flex-order: 3; + order: 3; + } + .clr-order-xl-4 { -webkit-box-ordinal-group: 5; -ms-flex-order: 4; order: 4; -} -.clr-order-5 { + } + .clr-order-xl-5 { -webkit-box-ordinal-group: 6; -ms-flex-order: 5; order: 5; -} -.clr-order-6 { + } + .clr-order-xl-6 { -webkit-box-ordinal-group: 7; -ms-flex-order: 6; order: 6; -} -.clr-order-7 { + } + .clr-order-xl-7 { -webkit-box-ordinal-group: 8; -ms-flex-order: 7; order: 7; -} -.clr-order-8 { + } + .clr-order-xl-8 { -webkit-box-ordinal-group: 9; -ms-flex-order: 8; order: 8; -} -.clr-order-9 { + } + .clr-order-xl-9 { -webkit-box-ordinal-group: 10; -ms-flex-order: 9; order: 9; -} -.clr-order-10 { + } + .clr-order-xl-10 { -webkit-box-ordinal-group: 11; -ms-flex-order: 10; order: 10; -} -.clr-order-11 { + } + .clr-order-xl-11 { -webkit-box-ordinal-group: 12; -ms-flex-order: 11; order: 11; -} -.clr-order-12 { + } + .clr-order-xl-12 { -webkit-box-ordinal-group: 13; -ms-flex-order: 12; order: 12; -} -.clr-offset-1 { + } + .clr-offset-xl-0 { + margin-left: 0; + } + .clr-offset-xl-1 { margin-left: 8.3333333333%; -} -.clr-offset-2 { + } + .clr-offset-xl-2 { margin-left: 16.6666666667%; -} -.clr-offset-3 { + } + .clr-offset-xl-3 { margin-left: 25%; -} -.clr-offset-4 { + } + .clr-offset-xl-4 { margin-left: 33.3333333333%; -} -.clr-offset-5 { + } + .clr-offset-xl-5 { margin-left: 41.6666666667%; -} -.clr-offset-6 { + } + .clr-offset-xl-6 { margin-left: 50%; -} -.clr-offset-7 { + } + .clr-offset-xl-7 { margin-left: 58.3333333333%; -} -.clr-offset-8 { + } + .clr-offset-xl-8 { margin-left: 66.6666666667%; -} -.clr-offset-9 { + } + .clr-offset-xl-9 { margin-left: 75%; -} -.clr-offset-10 { + } + .clr-offset-xl-10 { margin-left: 83.3333333333%; -} -.clr-offset-11 { + } + .clr-offset-xl-11 { margin-left: 91.6666666667%; -} -@media (min-width: 576px) { - .clr-col-sm { - -ms-flex-preferred-size: 0; - flex-basis: 0; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - max-width: 100%; - } - .clr-col-sm-auto { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: auto; - max-width: none; - } - .clr-col-sm-1 { - -webkit-box-flex: 0; - -ms-flex: 0 0 8.3333333333%; - flex: 0 0 8.3333333333%; - max-width: 8.3333333333%; - } - .clr-col-sm-2 { - -webkit-box-flex: 0; - -ms-flex: 0 0 16.6666666667%; - flex: 0 0 16.6666666667%; - max-width: 16.6666666667%; - } - .clr-col-sm-3 { - -webkit-box-flex: 0; - -ms-flex: 0 0 25%; - flex: 0 0 25%; - max-width: 25%; - } - .clr-col-sm-4 { - -webkit-box-flex: 0; - -ms-flex: 0 0 33.3333333333%; - flex: 0 0 33.3333333333%; - max-width: 33.3333333333%; - } - .clr-col-sm-5 { - -webkit-box-flex: 0; - -ms-flex: 0 0 41.6666666667%; - flex: 0 0 41.6666666667%; - max-width: 41.6666666667%; - } - .clr-col-sm-6 { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; - } - .clr-col-sm-7 { - -webkit-box-flex: 0; - -ms-flex: 0 0 58.3333333333%; - flex: 0 0 58.3333333333%; - max-width: 58.3333333333%; - } - .clr-col-sm-8 { - -webkit-box-flex: 0; - -ms-flex: 0 0 66.6666666667%; - flex: 0 0 66.6666666667%; - max-width: 66.6666666667%; - } - .clr-col-sm-9 { - -webkit-box-flex: 0; - -ms-flex: 0 0 75%; - flex: 0 0 75%; - max-width: 75%; - } - .clr-col-sm-10 { - -webkit-box-flex: 0; - -ms-flex: 0 0 83.3333333333%; - flex: 0 0 83.3333333333%; - max-width: 83.3333333333%; - } - .clr-col-sm-11 { - -webkit-box-flex: 0; - -ms-flex: 0 0 91.6666666667%; - flex: 0 0 91.6666666667%; - max-width: 91.6666666667%; - } - .clr-col-sm-12 { - -webkit-box-flex: 0; - -ms-flex: 0 0 100%; - flex: 0 0 100%; - max-width: 100%; - } - .clr-order-sm-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-order-sm-last { - -webkit-box-ordinal-group: 14; - -ms-flex-order: 13; - order: 13; - } - .clr-order-sm-0 { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } - .clr-order-sm-1 { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-order-sm-2 { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - } - .clr-order-sm-3 { - -webkit-box-ordinal-group: 4; - -ms-flex-order: 3; - order: 3; - } - .clr-order-sm-4 { - -webkit-box-ordinal-group: 5; - -ms-flex-order: 4; - order: 4; - } - .clr-order-sm-5 { - -webkit-box-ordinal-group: 6; - -ms-flex-order: 5; - order: 5; - } - .clr-order-sm-6 { - -webkit-box-ordinal-group: 7; - -ms-flex-order: 6; - order: 6; - } - .clr-order-sm-7 { - -webkit-box-ordinal-group: 8; - -ms-flex-order: 7; - order: 7; - } - .clr-order-sm-8 { - -webkit-box-ordinal-group: 9; - -ms-flex-order: 8; - order: 8; - } - .clr-order-sm-9 { - -webkit-box-ordinal-group: 10; - -ms-flex-order: 9; - order: 9; - } - .clr-order-sm-10 { - -webkit-box-ordinal-group: 11; - -ms-flex-order: 10; - order: 10; - } - .clr-order-sm-11 { - -webkit-box-ordinal-group: 12; - -ms-flex-order: 11; - order: 11; - } - .clr-order-sm-12 { - -webkit-box-ordinal-group: 13; - -ms-flex-order: 12; - order: 12; - } - .clr-offset-sm-0 { - margin-left: 0; - } - .clr-offset-sm-1 { - margin-left: 8.3333333333%; - } - .clr-offset-sm-2 { - margin-left: 16.6666666667%; - } - .clr-offset-sm-3 { - margin-left: 25%; - } - .clr-offset-sm-4 { - margin-left: 33.3333333333%; - } - .clr-offset-sm-5 { - margin-left: 41.6666666667%; - } - .clr-offset-sm-6 { - margin-left: 50%; - } - .clr-offset-sm-7 { - margin-left: 58.3333333333%; - } - .clr-offset-sm-8 { - margin-left: 66.6666666667%; - } - .clr-offset-sm-9 { - margin-left: 75%; - } - .clr-offset-sm-10 { - margin-left: 83.3333333333%; - } - .clr-offset-sm-11 { - margin-left: 91.6666666667%; - } -} -@media (min-width: 768px) { - .clr-col-md { - -ms-flex-preferred-size: 0; - flex-basis: 0; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - max-width: 100%; - } - .clr-col-md-auto { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: auto; - max-width: none; - } - .clr-col-md-1 { - -webkit-box-flex: 0; - -ms-flex: 0 0 8.3333333333%; - flex: 0 0 8.3333333333%; - max-width: 8.3333333333%; - } - .clr-col-md-2 { - -webkit-box-flex: 0; - -ms-flex: 0 0 16.6666666667%; - flex: 0 0 16.6666666667%; - max-width: 16.6666666667%; - } - .clr-col-md-3 { - -webkit-box-flex: 0; - -ms-flex: 0 0 25%; - flex: 0 0 25%; - max-width: 25%; - } - .clr-col-md-4 { - -webkit-box-flex: 0; - -ms-flex: 0 0 33.3333333333%; - flex: 0 0 33.3333333333%; - max-width: 33.3333333333%; - } - .clr-col-md-5 { - -webkit-box-flex: 0; - -ms-flex: 0 0 41.6666666667%; - flex: 0 0 41.6666666667%; - max-width: 41.6666666667%; - } - .clr-col-md-6 { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; - } - .clr-col-md-7 { - -webkit-box-flex: 0; - -ms-flex: 0 0 58.3333333333%; - flex: 0 0 58.3333333333%; - max-width: 58.3333333333%; - } - .clr-col-md-8 { - -webkit-box-flex: 0; - -ms-flex: 0 0 66.6666666667%; - flex: 0 0 66.6666666667%; - max-width: 66.6666666667%; - } - .clr-col-md-9 { - -webkit-box-flex: 0; - -ms-flex: 0 0 75%; - flex: 0 0 75%; - max-width: 75%; - } - .clr-col-md-10 { - -webkit-box-flex: 0; - -ms-flex: 0 0 83.3333333333%; - flex: 0 0 83.3333333333%; - max-width: 83.3333333333%; - } - .clr-col-md-11 { - -webkit-box-flex: 0; - -ms-flex: 0 0 91.6666666667%; - flex: 0 0 91.6666666667%; - max-width: 91.6666666667%; - } - .clr-col-md-12 { - -webkit-box-flex: 0; - -ms-flex: 0 0 100%; - flex: 0 0 100%; - max-width: 100%; - } - .clr-order-md-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-order-md-last { - -webkit-box-ordinal-group: 14; - -ms-flex-order: 13; - order: 13; - } - .clr-order-md-0 { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } - .clr-order-md-1 { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-order-md-2 { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - } - .clr-order-md-3 { - -webkit-box-ordinal-group: 4; - -ms-flex-order: 3; - order: 3; - } - .clr-order-md-4 { - -webkit-box-ordinal-group: 5; - -ms-flex-order: 4; - order: 4; - } - .clr-order-md-5 { - -webkit-box-ordinal-group: 6; - -ms-flex-order: 5; - order: 5; - } - .clr-order-md-6 { - -webkit-box-ordinal-group: 7; - -ms-flex-order: 6; - order: 6; - } - .clr-order-md-7 { - -webkit-box-ordinal-group: 8; - -ms-flex-order: 7; - order: 7; - } - .clr-order-md-8 { - -webkit-box-ordinal-group: 9; - -ms-flex-order: 8; - order: 8; - } - .clr-order-md-9 { - -webkit-box-ordinal-group: 10; - -ms-flex-order: 9; - order: 9; - } - .clr-order-md-10 { - -webkit-box-ordinal-group: 11; - -ms-flex-order: 10; - order: 10; - } - .clr-order-md-11 { - -webkit-box-ordinal-group: 12; - -ms-flex-order: 11; - order: 11; - } - .clr-order-md-12 { - -webkit-box-ordinal-group: 13; - -ms-flex-order: 12; - order: 12; - } - .clr-offset-md-0 { - margin-left: 0; - } - .clr-offset-md-1 { - margin-left: 8.3333333333%; - } - .clr-offset-md-2 { - margin-left: 16.6666666667%; - } - .clr-offset-md-3 { - margin-left: 25%; - } - .clr-offset-md-4 { - margin-left: 33.3333333333%; - } - .clr-offset-md-5 { - margin-left: 41.6666666667%; - } - .clr-offset-md-6 { - margin-left: 50%; - } - .clr-offset-md-7 { - margin-left: 58.3333333333%; - } - .clr-offset-md-8 { - margin-left: 66.6666666667%; - } - .clr-offset-md-9 { - margin-left: 75%; - } - .clr-offset-md-10 { - margin-left: 83.3333333333%; - } - .clr-offset-md-11 { - margin-left: 91.6666666667%; - } -} -@media (min-width: 992px) { - .clr-col-lg { - -ms-flex-preferred-size: 0; - flex-basis: 0; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - max-width: 100%; - } - .clr-col-lg-auto { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: auto; - max-width: none; - } - .clr-col-lg-1 { - -webkit-box-flex: 0; - -ms-flex: 0 0 8.3333333333%; - flex: 0 0 8.3333333333%; - max-width: 8.3333333333%; - } - .clr-col-lg-2 { - -webkit-box-flex: 0; - -ms-flex: 0 0 16.6666666667%; - flex: 0 0 16.6666666667%; - max-width: 16.6666666667%; - } - .clr-col-lg-3 { - -webkit-box-flex: 0; - -ms-flex: 0 0 25%; - flex: 0 0 25%; - max-width: 25%; - } - .clr-col-lg-4 { - -webkit-box-flex: 0; - -ms-flex: 0 0 33.3333333333%; - flex: 0 0 33.3333333333%; - max-width: 33.3333333333%; - } - .clr-col-lg-5 { - -webkit-box-flex: 0; - -ms-flex: 0 0 41.6666666667%; - flex: 0 0 41.6666666667%; - max-width: 41.6666666667%; - } - .clr-col-lg-6 { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; - } - .clr-col-lg-7 { - -webkit-box-flex: 0; - -ms-flex: 0 0 58.3333333333%; - flex: 0 0 58.3333333333%; - max-width: 58.3333333333%; - } - .clr-col-lg-8 { - -webkit-box-flex: 0; - -ms-flex: 0 0 66.6666666667%; - flex: 0 0 66.6666666667%; - max-width: 66.6666666667%; - } - .clr-col-lg-9 { - -webkit-box-flex: 0; - -ms-flex: 0 0 75%; - flex: 0 0 75%; - max-width: 75%; - } - .clr-col-lg-10 { - -webkit-box-flex: 0; - -ms-flex: 0 0 83.3333333333%; - flex: 0 0 83.3333333333%; - max-width: 83.3333333333%; - } - .clr-col-lg-11 { - -webkit-box-flex: 0; - -ms-flex: 0 0 91.6666666667%; - flex: 0 0 91.6666666667%; - max-width: 91.6666666667%; - } - .clr-col-lg-12 { - -webkit-box-flex: 0; - -ms-flex: 0 0 100%; - flex: 0 0 100%; - max-width: 100%; - } - .clr-order-lg-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-order-lg-last { - -webkit-box-ordinal-group: 14; - -ms-flex-order: 13; - order: 13; - } - .clr-order-lg-0 { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } - .clr-order-lg-1 { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-order-lg-2 { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - } - .clr-order-lg-3 { - -webkit-box-ordinal-group: 4; - -ms-flex-order: 3; - order: 3; - } - .clr-order-lg-4 { - -webkit-box-ordinal-group: 5; - -ms-flex-order: 4; - order: 4; - } - .clr-order-lg-5 { - -webkit-box-ordinal-group: 6; - -ms-flex-order: 5; - order: 5; - } - .clr-order-lg-6 { - -webkit-box-ordinal-group: 7; - -ms-flex-order: 6; - order: 6; - } - .clr-order-lg-7 { - -webkit-box-ordinal-group: 8; - -ms-flex-order: 7; - order: 7; - } - .clr-order-lg-8 { - -webkit-box-ordinal-group: 9; - -ms-flex-order: 8; - order: 8; - } - .clr-order-lg-9 { - -webkit-box-ordinal-group: 10; - -ms-flex-order: 9; - order: 9; - } - .clr-order-lg-10 { - -webkit-box-ordinal-group: 11; - -ms-flex-order: 10; - order: 10; - } - .clr-order-lg-11 { - -webkit-box-ordinal-group: 12; - -ms-flex-order: 11; - order: 11; - } - .clr-order-lg-12 { - -webkit-box-ordinal-group: 13; - -ms-flex-order: 12; - order: 12; - } - .clr-offset-lg-0 { - margin-left: 0; - } - .clr-offset-lg-1 { - margin-left: 8.3333333333%; - } - .clr-offset-lg-2 { - margin-left: 16.6666666667%; - } - .clr-offset-lg-3 { - margin-left: 25%; - } - .clr-offset-lg-4 { - margin-left: 33.3333333333%; - } - .clr-offset-lg-5 { - margin-left: 41.6666666667%; - } - .clr-offset-lg-6 { - margin-left: 50%; - } - .clr-offset-lg-7 { - margin-left: 58.3333333333%; - } - .clr-offset-lg-8 { - margin-left: 66.6666666667%; - } - .clr-offset-lg-9 { - margin-left: 75%; - } - .clr-offset-lg-10 { - margin-left: 83.3333333333%; - } - .clr-offset-lg-11 { - margin-left: 91.6666666667%; - } -} -@media (min-width: 1200px) { - .clr-col-xl { - -ms-flex-preferred-size: 0; - flex-basis: 0; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - max-width: 100%; - } - .clr-col-xl-auto { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: auto; - max-width: none; - } - .clr-col-xl-1 { - -webkit-box-flex: 0; - -ms-flex: 0 0 8.3333333333%; - flex: 0 0 8.3333333333%; - max-width: 8.3333333333%; - } - .clr-col-xl-2 { - -webkit-box-flex: 0; - -ms-flex: 0 0 16.6666666667%; - flex: 0 0 16.6666666667%; - max-width: 16.6666666667%; - } - .clr-col-xl-3 { - -webkit-box-flex: 0; - -ms-flex: 0 0 25%; - flex: 0 0 25%; - max-width: 25%; - } - .clr-col-xl-4 { - -webkit-box-flex: 0; - -ms-flex: 0 0 33.3333333333%; - flex: 0 0 33.3333333333%; - max-width: 33.3333333333%; - } - .clr-col-xl-5 { - -webkit-box-flex: 0; - -ms-flex: 0 0 41.6666666667%; - flex: 0 0 41.6666666667%; - max-width: 41.6666666667%; - } - .clr-col-xl-6 { - -webkit-box-flex: 0; - -ms-flex: 0 0 50%; - flex: 0 0 50%; - max-width: 50%; - } - .clr-col-xl-7 { - -webkit-box-flex: 0; - -ms-flex: 0 0 58.3333333333%; - flex: 0 0 58.3333333333%; - max-width: 58.3333333333%; - } - .clr-col-xl-8 { - -webkit-box-flex: 0; - -ms-flex: 0 0 66.6666666667%; - flex: 0 0 66.6666666667%; - max-width: 66.6666666667%; - } - .clr-col-xl-9 { - -webkit-box-flex: 0; - -ms-flex: 0 0 75%; - flex: 0 0 75%; - max-width: 75%; - } - .clr-col-xl-10 { - -webkit-box-flex: 0; - -ms-flex: 0 0 83.3333333333%; - flex: 0 0 83.3333333333%; - max-width: 83.3333333333%; - } - .clr-col-xl-11 { - -webkit-box-flex: 0; - -ms-flex: 0 0 91.6666666667%; - flex: 0 0 91.6666666667%; - max-width: 91.6666666667%; - } - .clr-col-xl-12 { - -webkit-box-flex: 0; - -ms-flex: 0 0 100%; - flex: 0 0 100%; - max-width: 100%; - } - .clr-order-xl-first { - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - } - .clr-order-xl-last { - -webkit-box-ordinal-group: 14; - -ms-flex-order: 13; - order: 13; - } - .clr-order-xl-0 { - -webkit-box-ordinal-group: 1; - -ms-flex-order: 0; - order: 0; - } - .clr-order-xl-1 { - -webkit-box-ordinal-group: 2; - -ms-flex-order: 1; - order: 1; - } - .clr-order-xl-2 { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - } - .clr-order-xl-3 { - -webkit-box-ordinal-group: 4; - -ms-flex-order: 3; - order: 3; - } - .clr-order-xl-4 { - -webkit-box-ordinal-group: 5; - -ms-flex-order: 4; - order: 4; - } - .clr-order-xl-5 { - -webkit-box-ordinal-group: 6; - -ms-flex-order: 5; - order: 5; - } - .clr-order-xl-6 { - -webkit-box-ordinal-group: 7; - -ms-flex-order: 6; - order: 6; - } - .clr-order-xl-7 { - -webkit-box-ordinal-group: 8; - -ms-flex-order: 7; - order: 7; - } - .clr-order-xl-8 { - -webkit-box-ordinal-group: 9; - -ms-flex-order: 8; - order: 8; - } - .clr-order-xl-9 { - -webkit-box-ordinal-group: 10; - -ms-flex-order: 9; - order: 9; - } - .clr-order-xl-10 { - -webkit-box-ordinal-group: 11; - -ms-flex-order: 10; - order: 10; - } - .clr-order-xl-11 { - -webkit-box-ordinal-group: 12; - -ms-flex-order: 11; - order: 11; - } - .clr-order-xl-12 { - -webkit-box-ordinal-group: 13; - -ms-flex-order: 12; - order: 12; - } - .clr-offset-xl-0 { - margin-left: 0; - } - .clr-offset-xl-1 { - margin-left: 8.3333333333%; - } - .clr-offset-xl-2 { - margin-left: 16.6666666667%; - } - .clr-offset-xl-3 { - margin-left: 25%; - } - .clr-offset-xl-4 { - margin-left: 33.3333333333%; - } - .clr-offset-xl-5 { - margin-left: 41.6666666667%; - } - .clr-offset-xl-6 { - margin-left: 50%; - } - .clr-offset-xl-7 { - margin-left: 58.3333333333%; - } - .clr-offset-xl-8 { - margin-left: 66.6666666667%; - } - .clr-offset-xl-9 { - margin-left: 75%; - } - .clr-offset-xl-10 { - margin-left: 83.3333333333%; - } - .clr-offset-xl-11 { - margin-left: 91.6666666667%; - } + } } .clr-break-row { - width: 100%; + width: 100%; } *, :after, :before { - -webkit-box-sizing: border-box; - box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; } pre { - margin: 0.6rem 0; + margin: 0.6rem 0; } pre { - border-color: #ccc; - border-color: var(--clr-color-neutral-400, #ccc); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); + border-color: #ccc; + border-color: var(--clr-color-neutral-400, #ccc); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); } pre code { - white-space: pre; + white-space: pre; } code.clr-code { - color: #c21d00; - color: var(--clr-color-danger-800, #c21d00); - padding: 0; - background: 0 0; + color: #c21d00; + color: var(--clr-color-danger-800, #c21d00); + padding: 0; + background: 0 0; } ul.list-unstyled:not([cds-list]) { - padding-left: 0; - margin-left: 0; - list-style: none; + padding-left: 0; + margin-left: 0; + list-style: none; } ol:not([cds-list]), ul:not([cds-list]) { - list-style-position: inside; - margin-left: 0; - margin-top: 0; - margin-bottom: 0; - padding-left: 0; + list-style-position: inside; + margin-left: 0; + margin-top: 0; + margin-bottom: 0; + padding-left: 0; } ol.list:not([cds-list]), ul.list:not([cds-list]) { - list-style-position: outside; - margin-left: 1.1em; + list-style-position: outside; + margin-left: 1.1em; } ol.list:not([cds-list]).compact, ul.list:not([cds-list]).compact { - line-height: 0.9rem; + line-height: 0.9rem; } ol.list:not([cds-list]).compact > li, ul.list:not([cds-list]).compact > li { - margin-bottom: 0.3rem; + margin-bottom: 0.3rem; } ol.list:not([cds-list]).compact > li:last-child, ul.list:not([cds-list]).compact > li:last-child { - margin-bottom: 0; + margin-bottom: 0; } ol:not([cds-list]) > li > ul.list-unstyled, ul:not(.list-unstyled):not([cds-list]) > li > ul.list-unstyled { - margin-left: 1.1em; + margin-left: 1.1em; } li > ul:not([cds-list]) { - margin-top: 0; - margin-left: 1.1em; + margin-top: 0; + margin-left: 1.1em; } ul.list-group:not([cds-list]) { - margin-top: 0; + margin-top: 0; } ol:not([cds-list]).list-spacer, ul:not([cds-list]).list-spacer { - margin-top: 1.2rem; + margin-top: 1.2rem; } h1:not([cds-text]) { - color: #000; - color: var(--clr-h1-color, #000); - font-weight: 200; - font-weight: var(--clr-h1-font-weight, 200); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-h1-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 1.6rem; - letter-spacing: normal; - line-height: 2.4rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #000; + color: var(--clr-h1-color, #000); + font-weight: 200; + font-weight: var(--clr-h1-font-weight, 200); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-h1-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 1.6rem; + letter-spacing: normal; + line-height: 2.4rem; + margin-top: 1.2rem; + margin-bottom: 0; } h2:not([cds-text]) { - color: #000; - color: var(--clr-h2-color, #000); - font-weight: 200; - font-weight: var(--clr-h2-font-weight, 200); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-h2-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 1.4rem; - letter-spacing: normal; - line-height: 2.4rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #000; + color: var(--clr-h2-color, #000); + font-weight: 200; + font-weight: var(--clr-h2-font-weight, 200); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-h2-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 1.4rem; + letter-spacing: normal; + line-height: 2.4rem; + margin-top: 1.2rem; + margin-bottom: 0; } h3:not([cds-text]) { - color: #000; - color: var(--clr-h3-color, #000); - font-weight: 200; - font-weight: var(--clr-h3-font-weight, 200); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-h3-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 1.1rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #000; + color: var(--clr-h3-color, #000); + font-weight: 200; + font-weight: var(--clr-h3-font-weight, 200); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-h3-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 1.1rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } h4:not([cds-text]) { - color: #000; - color: var(--clr-h4-color, #000); - font-weight: 200; - font-weight: var(--clr-h4-font-weight, 200); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-h4-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 0.9rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #000; + color: var(--clr-h4-color, #000); + font-weight: 200; + font-weight: var(--clr-h4-font-weight, 200); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-h4-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 0.9rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } h5:not([cds-text]) { - color: #666; - color: var(--clr-h5-color, #666); - font-weight: 400; - font-weight: var(--clr-h5-font-weight, 400); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-h5-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 0.8rem; - letter-spacing: 0.01em; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-h5-color, #666); + font-weight: 400; + font-weight: var(--clr-h5-font-weight, 400); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-h5-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 0.8rem; + letter-spacing: 0.01em; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } h6:not([cds-text]) { - color: #333; - color: var(--clr-h6-color, #333); - font-weight: 500; - font-weight: var(--clr-h6-font-weight, 500); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-h6-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 0.7rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #333; + color: var(--clr-h6-color, #333); + font-weight: 500; + font-weight: var(--clr-h6-font-weight, 500); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-h6-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 0.7rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body:not([cds-text]) { - color: #666; - color: var(--clr-p1-color, #666); - font-weight: 400; - font-weight: var(--clr-p1-font-weight, 400); - font-size: 0.7rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-bottom: 0; - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-font, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - margin-top: 0 !important; + color: #666; + color: var(--clr-p1-color, #666); + font-weight: 400; + font-weight: var(--clr-p1-font-weight, 400); + font-size: 0.7rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-bottom: 0; + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-font, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + margin-top: 0 !important; } body p:not([cds-text]) { - color: #666; - color: var(--clr-p1-color, #666); - font-weight: 400; - font-weight: var(--clr-p1-font-weight, 400); - font-size: 0.7rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p1-color, #666); + font-weight: 400; + font-weight: var(--clr-p1-font-weight, 400); + font-size: 0.7rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p0:not([cds-text]), body p.p0:not([cds-text]) { - color: #666; - color: var(--clr-p0-color, #666); - font-weight: 200; - font-weight: var(--clr-p0-font-weight, 200); - font-size: 1rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p0-color, #666); + font-weight: 200; + font-weight: var(--clr-p0-font-weight, 200); + font-size: 1rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p2:not([cds-text]), body p.p2:not([cds-text]) { - color: #666; - color: var(--clr-p2-color, #666); - font-weight: 500; - font-weight: var(--clr-p2-font-weight, 500); - font-size: 0.65rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p2-color, #666); + font-weight: 500; + font-weight: var(--clr-p2-font-weight, 500); + font-size: 0.65rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p3:not([cds-text]), body p.p3:not([cds-text]) { - color: #666; - color: var(--clr-p3-color, #666); - font-weight: 400; - font-weight: var(--clr-p3-font-weight, 400); - font-size: 0.65rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p3-color, #666); + font-weight: 400; + font-weight: var(--clr-p3-font-weight, 400); + font-size: 0.65rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p4:not([cds-text]), body p.p4:not([cds-text]) { - color: #666; - color: var(--clr-p4-color, #666); - font-weight: 600; - font-weight: var(--clr-p4-font-weight, 600); - font-size: 0.6rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p4-color, #666); + font-weight: 600; + font-weight: var(--clr-p4-font-weight, 600); + font-size: 0.6rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p5:not([cds-text]), body p.p5:not([cds-text]) { - color: #666; - color: var(--clr-p5-color, #666); - font-weight: 400; - font-weight: var(--clr-p5-font-weight, 400); - font-size: 0.6rem; - letter-spacing: normal; - line-height: 1.2rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p5-color, #666); + font-weight: 400; + font-weight: var(--clr-p5-font-weight, 400); + font-size: 0.6rem; + letter-spacing: normal; + line-height: 1.2rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p6:not([cds-text]), body p.p6:not([cds-text]) { - color: #666; - color: var(--clr-p6-color, #666); - font-weight: 600; - font-weight: var(--clr-p6-font-weight, 600); - font-size: 0.55rem; - letter-spacing: 0.03em; - line-height: 0.6rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p6-color, #666); + font-weight: 600; + font-weight: var(--clr-p6-font-weight, 600); + font-size: 0.55rem; + letter-spacing: 0.03em; + line-height: 0.6rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p7:not([cds-text]), body p.p7:not([cds-text]) { - color: #666; - color: var(--clr-p7-color, #666); - font-weight: 400; - font-weight: var(--clr-p7-font-weight, 400); - font-size: 0.55rem; - letter-spacing: 0.03em; - line-height: 0.6rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p7-color, #666); + font-weight: 400; + font-weight: var(--clr-p7-font-weight, 400); + font-size: 0.55rem; + letter-spacing: 0.03em; + line-height: 0.6rem; + margin-top: 1.2rem; + margin-bottom: 0; } body .p8:not([cds-text]), body p.p8:not([cds-text]) { - color: #666; - color: var(--clr-p8-color, #666); - font-weight: 400; - font-weight: var(--clr-p8-font-weight, 400); - font-size: 0.5rem; - letter-spacing: 0.03em; - line-height: 0.6rem; - margin-top: 1.2rem; - margin-bottom: 0; + color: #666; + color: var(--clr-p8-color, #666); + font-weight: 400; + font-weight: var(--clr-p8-font-weight, 400); + font-size: 0.5rem; + letter-spacing: 0.03em; + line-height: 0.6rem; + margin-top: 1.2rem; + margin-bottom: 0; } .text-light { - font-weight: 200; - font-weight: var(--clr-font-weight-light, 200); + font-weight: 200; + font-weight: var(--clr-font-weight-light, 200); } .text-right { - text-align: right !important; + text-align: right !important; } .text-center { - text-align: center !important; + text-align: center !important; } .text-left { - text-align: left !important; + text-align: left !important; } .text-justify { - text-align: justify !important; + text-align: justify !important; } html:not([cds-text]) { - color: #666; - color: var(--clr-global-font-color, #666); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-font, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 125%; + color: #666; + color: var(--clr-global-font-color, #666); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-font, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 125%; } @font-face { - font-family: Metropolis; - src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAFQgABMAAAAAm8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcfNH55kdERUYAAAHEAAAATQAAAGIH1Qf8R1BPUwAAAhQAAAcaAAAOdjy+ejlHU1VCAAAJMAAAACAAAAAgRHZMdU9TLzIAAAlQAAAATQAAAGBoPqzrY21hcAAACaAAAAJsAAADnndDD7FjdnQgAAAMDAAAADAAAAA8EY4BjGZwZ20AAAw8AAAGOgAADRZ2ZH12Z2FzcAAAEngAAAAIAAAACAAAABBnbHlmAAASgAAANnMAAGgUxFIgN2hlYWQAAEj0AAAANgAAADYLYYgUaGhlYQAASSwAAAAhAAAAJAd2BDJobXR4AABJUAAAAogAAATuuPI/FGxvY2EAAEvYAAACcgAAAnqJanBwbWF4cAAATkwAAAAgAAAAIAKEAeluYW1lAABObAAAAYIAAANWLdCE9XBvc3QAAE/wAAADoQAABiGXFj2KcHJlcAAAU5QAAACBAAAAjRlQAhB3ZWJmAABUGAAAAAYAAAAG9nhYmAAAAAEAAAAA1FG1agAAAADTwZ2GAAAAANS+pvV42g2MQQqEQBDEEkf0MLPof7ypL/DofXfV/z/AIgRC0TQCLR6cdFRkjVso7HzTv1D4B7m4048DOlopNlv645SeXXLT51sXzSa+W3AF3AAAAHjajVcBbFbVFf7Oufe+v/0LWEoLCB0DUhkxTWWESUVGiWMFsVPDmEEHZlucY61Q7BjZiDFKHZql6YzDDpE0qAyMNsBQsSKypqvOOUdkY6YhYFwHyDYm07nFCPL2vfN+6F9ot/GFj8O59917zznf7bmFAMhiMhZC5tXWLUYBPD2IYzj+I1C4hm83rUTpim82NaB8RcOKBs4G/cloOiNhx++yGI0JmGIehwrUuY50NFplq0rUiogfyfDV/GKc+QJKL0BQG7eSA2ajBZ8ilnFQHoPzZKwcQRGG8WR/j7vj7XFvfBRD/Ik/GHLkt4N6+7h3/v+Pxz8dcoX3hhwZ+jx/jPcOMbI97ov3JbjI38u/v0kw2B5xK7OkmMhMT2G2PkcoqgiHqwiP6UTAF4gIM4kMriEKMIsoZG5ns1JrCMH9+BFnPkgEZryF/hcIwYuE4CVCcZDw+APhcZQI+DMR4TgR4T0iwmkigw+IDM4Qhazep1wtJrJSLMUolBIpIZdKKXkcK5vl2tOokgp+cyUhdu70xGondnZibycOmEcUoJYoxAIii0VEERrxfa6QRBJZJJFFEvATPMr5bUQhfoZNnP8Efs7524ki7CQy2EUU4BdEBruJAjxHZPA8UYA9RCE6iULsJ7LoIrLoJrLoIbJ4lRD8mhDLToR3iCL8iUjzopYXtbx4y0uwvATLi7e8eMuLlzEyhvm6XC4nJzkKXLWKGZrCGlexttNZ05nMzCxmpBGrcDea8D2sZi3vxzo04wFm4UFGv5MRPcdKvsgKHmTljrJix1mp0zzJGbtZxdy3NLlfeq/dw9ekiXEPp7r2UXet8b8GUauNDHYHLoycSjDIl6eHvBunziue9/po3Bw3XzyS3rp4c7x50JG/2DeKctOEmCYUXyMcbiU8biMClhIRFfEo5yRqEFODmhoUHUSEHURklRartFilxSotVlfFMcLhBOFwknA4SwScIyIZKSNZ11EyilwmZeSkomIVFRkv47m+ohKXEUUYSQwzpYspXU3pLqf0+US+0r3FE+XFkzGlu5zS+zXuLSpvUWUsqvNKTzWeqnsvMbSuk2i9aVZzESbKdRZnxvTrTL+aizlRseYiT7SsefHnK9pZFjIyTa7h7slPr1pGuIj1upVxLWUkbYxkIx5jNE/gSTyFrYxoOyPZwdvYydN28ZQ9PN0x1uAkT3aOJxjF3cZwl/FccYLpWTHCOkqJqaOEu9TQErNr2ImORBPIfcx/t6yXFnlENkq7bJVnZJfskX3SLa/LATkkh+VdOSGn5EP5WM6p16wW62gt18k6Vat0hs7SuVqrdbpIl+jteofW6ypdo/foOn1IW3WDbtItuk07dLd26n7t0Tf0LX1bj2ifntT39SP9xMFFbpgrcWPdBFfhrnTT3NVutrvOLXA3usXuNvcNd6e7yzW5H7h73QPux+5h1+Y2uyfd026He97tdV3uNfem+73rde+4Y+6v7h/u3+6sV1/gR/hSP85P9FN8pZ/uq/0cP88v9Df7W/xS/y2/3K/0q/1af59f71v8I36jb/db/TN+l9/j9/lu/7o/4A/5w/5df8Kf8h/6j/254EM2FIfRoTxMDlNDVZgRZoW5oTbUhUVhSbg93BHqw6qwJtwT1oWHQmvYEDaFLWFb6Ai7Q2fYH3rCG+Gt8HY4EvrCyfB++Ch8EiGKomG8HU26k9xsPN+4xnhDwmg0bjPP2n5/jm8wrjS+1nhlwlpm9vXGc4wrlD9T5Qrjq4yrE0a9cbu+TG4wf6XxbPP3Gp8xz0Tjx40nGUfGC1w9+SnjpsFZf5UXY435L2F82XiLcWM/y7I0drPvMn7VeMOlnGbA7Ev5euMK7fpfrM8OyFXXYIy5xo8b1/czs9dl2fvvnOaza1CelMdNeWceYOfVtNn8V5g/355vOWwxe1le5tMoBtg2mqoi359mtTq1bU6qmbQ6adSpPnN2zp/MaTO73a05r96cxg6a3ZPYuZqmGVubU3K4yJ+eIc3bAbPXWo0OJfNdqsAbbK9em2M3ItXtAH+l2WdyNdp5QdX5/vQGXZunmbQi+fZWs7+e+m2+3QstM7/pIae0SXn2HOPIPEPZfzP7bouu1uw3zc5fuSb+Jbkq3n9RpfJ3rM7d7q7/gwfOVHzW3qXgu2sqs5K8Tj27diW7YPLCjvB5vsQymIFq9rCkc49g3/4ie3rSuUfaG7XEOvco/l61kH2ojijDTexzo9npbuHvPkuIcuvln2HXW8au1ci320R7vc1kR2/nelvY+b5ive+r7H4vs0O+ggP4Dl90p/FDe1VuxD8l4DF24vHosJ7ayfOKXGa/m0WQeL2p8D7cSV7PbleEsdyrghFNw9U89XU85Y1YzNEXTLu/Mz5sbHeGGu3ng8bLjbcZ9xmfNS7Cl7jPcnxXMlIghZKVIhkuIy490X8AtlKXWAAAAAEAAAAKABwAHgABREZMVAAIAAQAAAAA//8AAAAAAAB42mNgZrJgnMDAysDC1MUUwcDA4A2hGeMYRBjNgHygFBywMyCBUO9wPwYHBgXVP8zS/40ZGJiPMqoqMDBMBskxsTKtB1IKDEwAxlcKNgAAAHjatZNZUI5RHMZ//7d9ESoU9fb2adNGohRF9qXIvpSs2bKv2RrrEENFUsieJKMZE1NTthvuuDVjjL7PlVvuDB3HV0wzzLhyZt5z3nPOnOeceZ7fH3Ch6wtBdI9U6pk4565SrMcljMONgZRwizru0kgTzbTQJh4SIIMkTAZLnCRJqqRLpkyVHMmTQimSEiPVeGW8d4kyj5ut5hPzi+VuBVrBVqhls6KsYVa6dd/mH/lNKX2HxY0e2o9p45n4Sn8xxSaxkigpkiYZkiXZkisFskE2a+2XxlutfchsMdvNz5ZhBVhBVohTe6iV9ktbfVQv1HP1VLWrVvVINauHqkk1qgZVr+rUNVWralS1qlKVqkKVqTOqVJ3ofNOZ1Zn0/ZOj3FHgyHfE2Afa/ew+di+7m93o+NrxuePwh5B3yV1e/afmbng7k+CPWwSj+8/4h0bXSRdcdXbueOCJF9744Esv/OhNH/riTwCB9KM/AwgiWGc8SKceikmYTiQcG4OJIJIooolhCLHEEU8CiQxlGEkMJ5kRjCSFVEaRRjqjGUMGmYzVzGQxnglMZBKTmcJUpjGdGWSTw0xmkcts5jCXecxnAQtZxGJNWh75LKWAZSxnhX7/Dnaym2IOcZzTlFNGBec5RyVVVHORGi5xhcvUcpXr3NQU/WT0Ng2apXuapp9tFau1HdFs4Gy3N+tZo/tdnPjtVuFfHLxAPZtZ2WNlLZskRo9b2M4x7DgkXPMZKVG6AiK4o3ceoGmWBF0P8d1nipxhxLKNvWxlH3s4wEFdS/s5wlG9dZhSTnGS17qaerFOvMRbfNgofpp/zx+QzaroeNpjYMACHIHQksGSaT0DA9NuJlYGhv8hzNL/jZl2///CdIBJ8P+X/34gPgDIPQ0ieNqtVml300YUlbxlIxtZaFFLx0ycptHIpBSCAQNBiu1CujhbK0FppThJ9wW60X1f8K95ctpz6Dd+Wu8b2SaBhJ721B/07sy7M2+beWMylCBj3a8EQizdNYaWlyi3es2nUxbNBOG2aK77lCpEf/UavUajITesfJ6MgAxPLrYM0/BC1yFTkQi3HUopsSnoXp0y09daM2a/V2lUKFfx85QuBCvX/bzMW01fUL2OqYXAElRiVAoCESfsaJNmMNUeCZpj/Rwz79V9AW+akaD+uh9iRrCun9E8o/nQCoMgsMi0g0CSUfe3gsChtBLYJ1OI4FnWq/uUlS7lpIs4AjJDhzJKwi+xGWc3XMEa9thKPOAvSJUGpWfzUHqiKZowEM9lCwhy2Q/rVrQS+DLIB4IWVn3oLA6tbd+hrKIez24ZqSRTOQylK5Fx6UaU2tgmswEDlJ11qEcJdnXAa9zNGBuCd6CFMGBKuKhd7VWtngHDq7iz+W7u+9TeWvQnu5g2XPAQdygqTRlxXXS+DItzSsKCkx0vUR0ZLSYmBg5YTlNYZVj3Q9u96JDSAbUG+tMotiXzwWzeoUEVp1IV2owWHRpSIApBh7yrvBxAugEN8mgFo0GMHBrGNiM6JQIZaMAuDXmhaIaChpA0h0bU0pofZzYXgyka3JK3HRpVS8v+0moyaeUxP6bnD6vYGPbW/Xh4GAWMXBq2+cziJLvxIf4M4kPmJCqRLtT9mJOHaN0m6stmZ/MSyzrYSvS8BFeBZwJEUoP/NczuLdUBBYwNY0wiWx4ZF1umaepajSkjNlKVNZ+GpSsqNIDD1w/DoStCmP9zdNQ0hgzXbYbx4ZxNd2zrONI0jtjGbIcmVGyynESeWR5RcZrlYyrOsHxcxVmWR1WcY2mpuIflEyruZfmkivtYPqNkJ++UC5FhKYpk3uAL4tDsLuVkV3kzUdq7lNNd5a1EeUwZNGj/h/ieQnzH4JdAfCzziI/lccTHUiI+llOIj2UB8bGcRnwsn0Z8LGcQH0ulRFkfU0fB7GgoPHbB06XE1VN8VouKHJsc3MITuAA1cUAVZVSS3BEfybA4+rluac1JOjEbZ82Jio9GxgE+uzszD6tPKnFa+/sceGblYSO4nfsa53lj8g+Df4sXZSk+aU5wcKeQAHi8v8O4FVHJodOqeKTs0Pw/UXGCG6CfQU2MyYIoihrffOTySrNZkzW0Ch9PBDor2sG8aU6MI6UltKhJGgEtg65Z0DTq8+ytZlEKUW5iv7N7KaKY7EUZzIApKOSmsbDs76REWlg7qen00cDlRtqLniw1W1Zxhb0H72PIzSx5N1JeuCkp7UWbUKe8yAIOuZE9uCaCW2jvsopiSlioIj4IbQX77WNEJi0zgy6BImRxsrIP7YodOaKCdgLfetIq79tC7c918iAwm51u50GWkaLzXRX1an1V1tgoV6/cTR8H086wseYXRRlPLnvfnhTsV6cEuQJGV3a/7knx9jvW7UpJPtsXdnnidUoV8l+AB0PulPciGkWRs1ilEc+vW3gyRTkoxkVzHBf00h7tilXfo13Yd+2jVlxWVLIfZdBVdNZuwjc+XwjqQCoKWqQiVng6ZD6bnZrwsZS4LEXcs2TXRfQdPCEd4r84xLX/69xyFNyiyhJdaNcJyQdtHyvorSW7k4cqRmftvGxnoh1JN+gagp5ILjj+XuAujxXpFO7z8wfMX8F25vgYnQa+qugMxBLnrYIEiyre0k6mXlB8hGkJ8EXVQrMCeAnAZPCyapl6pg6gZ5aZUwFYYQ6DVeYwWGMOg3W1g653GegVIFOjV9WOmcz5QMlcwDyT0TXmaXSdeRq9xjyNbrBND+B1tsngDbbJIGSbDCLmVAE2mMOgwRwGm8xhsKX9coG2tV+M3tR+MXpL+8Xobe0Xo3e0X4ze1X4xek/7xeh95Phct4Af6BFdBPwwgZcAP+Kk69ECRjfxjLY5txLInI81x2xzPsHi891dP9UjveKzBPKKzxPI9NvYp034IoFM+DKBTPgK3HJ3v6/1SNO/SSDTv00g07/Dyjbh+wQy4YcEMuFHcC909/tJjzT95wQy/ZcEMv1XrGwTfksgE35PIBPuqJ2+TKrzZ9W1qXeL0lP125132PkbZTO6LAAAAAEAAf//AA942rV9CXhbV5noOedKupIl2b5aLcubrNXWamuzvMjXS7wvcbzFSRxnc5y0KV3Sli4hpLQNFAqUAWZYhr4u0KFMS5K2dKHtFChQ2qHLDG+AecMH5Q0zLG/YBjowbX09/zn3Xlm27KbwfS+1JPvqrP/59+UUlaHFtST+FOdGHKpAduRCXtSE0iiHutAQcomO3nxnezaTbA766qurHEKlQUNQWSKs9Qge3p60e+3JtDedTPPsk4df1af0Gf2k38CTtPq70oZ1yCbT+FPSs7jrP3t67+vtve++3kaPp7e390iv59b7jjR6jnjuu+8+z5Ejtw4M3Hd0oOFF7md9Hr8Hfm461jgw4DsIvw14Onsbj9zQ5ozvvPLKB6+8cmd8xRP3wA9CBE2v/R6dI+fY3vxiI8IYLSKEykcRIdySBnOckxvTaDQVmnK/UKnlnWFrkvM6AulUJtnqsNt03v3D5oTG7bZZq6ut5JxkedFts7jdFpsbobU1NIgfxWPkk5WNqAyhSg7eH0F03iC8XQfzulE9umH0vHfnbjFUoSNIyxFMEF42YoOhfLS80szxvH7RVEb0emFUgwmpIGNuMUAfsC+Rnj9a2k9puCDWIVRfV1sD07irXVVOWLBVKPzja8OYT/Je3ptlr2ySvZI8e/H0S/ybzI3mG2Ntsbvgda352sw7zdcpf91gfviuzF34a99NPwb/0t9NPw7/0t8FSMbWHiUR8gfkQSEUR51iLub31dVWu5w2s6nMYOaIDiOOjCDCkZsRRvgWCma0BHBxorGmpqZ4U9xhFyxa3hH2lWPAkXQMB7MOpxDD6VQeZwFZHE4engl12I4cznQ5fGTSqUBQIJHUkR1iz8loaOfxowda9+bEKxZDvpno5e+Q9os7OkaweWZ06ua5NNfdy2cjrTsrcWX1rqHkbFrX2WWcbfVGeOlN9+5JXJO2/Z4fbpWyI6mWDof0JqxNi+Jr/0X+lXwJMMUKpxZHHeiF0fNVcHJhI9aWYazT4hWkQ3qDTn8MaTRoiWCKSWY4Em7JxBOOq+DG3KPng9Al/pZdEDRl/fglE+Z5Jw9nntyiByHQlnZDW/daWBADiURDg82GUKIj0Z5JNcQbYqGArd5WV11ltQiVsJnyQDlvD1tk4CZbAaa2cuzFSey1UKB6G3V2myOJNn2fx+vf/fVAMDQYiQ7CexR/qFNq7rwm1x6JtLWH8dRAKDgYlb+KtdGHuQhejE7EW3ZGo5OJloko3rU6hT84kM4MDmTTA9Lx6ERLYiIWnYy3TEYTA5n0IP0K9sehprXfkxPkMYB9GPhPXuyIR/11NW6X04DLOFLPkIpiPl4CMnaMaouwKhJpaIikI6mGcENzuFEHmKUN6ryNdE/Z4o3BN85M1qnjnYjuzCnv0pINBOHPOpwkiaV9JzuP5O7x1EXGE5Hh8Nxc5spIUyaRvFr6dFdt/URfrjk0fjp/tnmomT+w3LKQu2kgOuiLjITDI835UZ/4TvFA1fHha8jx9lhNV6ghG27uWj03d8tY1/6Q6AViAH6BPkE+iUwoOnreAYhipCyJnS8GJKhE9M95+BMvUtIZX3jE7iOwHWuBI1Vg72x5lSA4y/kg/o9D3soqp+A9lIHW+bU5HCNPwdi6h01anAgzNpZ1AvnYnPznL7nksHdu4Zx3149vuOHHu/27v37VDy4EYCKg5zlcp/bTQT8nI7asDLDYuYU572HofMuFH1z1dejHutN+Hfgm4H1fQrNIFLv8GJPJHd2xRpdNq0d4F9YiboTDWIORFqMVHdZqyRJFaO0Y7G4WzQwPZdJNwdoaN0+xE1bajXngBnBqQfgjm+nGwYD8W7LV6ajHwSIIZLLAJthzuw06VWCH08F+Z73hPydj3V82G3TeioZyrcbMa8qc0XAkZi/T8GatxtLs0xnMwKz0uspKLmTTas08VyaEdRGnI+wo43izTuMIOYw6vdmAbzLra1qaa8xVPGfSa4y8WRAsFiNv1OhNXFljTXNLjd5s1jta46ZyrsHImXRaE18mEGgimHiTVmfiyhss+nirQ2+mB4v60HFSTlLIiAKAExoOa2YBl4EzYA4tA7nTo9fgccbDK3V8dRh7qfRMUymaJOXfzD/3XP6bOP5N+gsdb3LtNHoM7UTlyCkC6aJBFaWAMHwWij2A843rIo2fDJqtINGcNfWh49EEFW3+ulqxlY7Vhn6LO3AMqLFKtDNknF1HRqGSoaLH7mnDnLSKY11sP8MgYz8H8xuptkCfAJfD2InpQRtRmY+DAy6WqEcUaWoqSFK89pu1R/HHyU9hXkEsp4OC5MBXyxNSVoUnB6S7B8hP33wZMfnaAbziGHkS2Fs9iohNgFhsxzJXwCDY2QIqgZ4q6yvrqhyMD+pgIZrNfI6kUzHsbaREAgwA43cPDr57aur04ODpqfzBTOZgPn8okzmUN+29Z2Xl7r17715ZuWdv59jZudn3jo6enZs7O8ZgUA3vb4C81yG3WAVHyQG8RmSujQE/xwQrAx0fzCYF75O31D6YEsmB2ZaTq1OI9W+BTelhPy7ULAaNZbAdwAZCRhg4GbujCssiYjJGcPqCGr4qnPVT/kVXzwcLHK4Cw/m0wKn0Nu1oumJnLHr9cOeh3NjMt/BJqantX/KXppp6A5f7xf7UYq7v1NCD8hmGAJ5amD+MusVOdzXM5QOChhXAMkCSQwsCconjNEuAoLAYkJdLlKadjJzDqNnn9/j9PO+Ck6acI9nKWGsYp52tGXmNOj6YUbmwAvxXTkWivqP5xFj40NyOgbF9zcORzEJTePH9+SPtA22dU52X9Jp62puTWX9PU763E/d3+vO+dCp0KjGX7tollM/2ZfemGD6E4C0F8C9DZhQVm00YUHBEp4V9YAT66lEAIROlFZoxo9FoNgINC5ZKul6/J4iTAtVYvWkB49ukzxvwzPill4rSzx9swy9IuY4Hf4ZvkM7K59QGcHLCPA1U73EAexMqOaJBGE4K3pGGAYksypJ7I5AaUH2jYA2UAEk+P0pVfKaAlj9+V6S5eaU9PhGJTCQmR1sN+IPSQ3zfXOdyZ/5Er6k9GY8lw2PRyFAoW4WXul5vSR/I96y0M1h0whr9cJZu0ON7xW4gIo4EqohWQ0Z0wGI0Wk6zggpilIcFqtyipgahmqaakK8ROld7/QE9KMRIWRLlIBTLnHyAkksthj8s6gbokSbxe452dZ3oPX1m8NTYO2Z9o3O5/Znqy3r9E5HYRLz/sLly3xB+MHOwO3+s68k7V/7mwK7W4anb52zpbulMfLw5Ptw03rV7WYZzB2zEwOipXqwBGYIZNVE+hJnuDtovaDccLA970h47MIinpHfjl6TvDJOrulpW30P1iRTAoYbBIQzj9Ys9Nh0BbjvCFwFB4Rp6AAKGs2JcIxKhcIh0RNpTrTXhmmYZGhEDZWOUe2Q2qRQKfAqwkFmJU/k7WE4YZ/nJ5T2zvZ3D3cc6Oo91j7X3zvZc0dc0Eo+MREB7iI+EWnYlUtORyK50YleLqWU+27Xf7pjLpqZjsZlUbtbm2N+ZnW/Bt3tyfl9HY2OHL5BrINIFXz4Q6vZi7O0OBfI+tNX511XC0Rv0YD8Q2LoWCFlLVoAUONgvxzlGi6SFev6NDXTHAa+fnb9DPn5Q0SnCdmH6hwIBC4XFOvP83KaT72L4sIOdfAvDAvLkxpOXsUE6Q09exgF5D0ym4NdBfsCyRs9HQVNyUqbOVirI1ptWtt7cVFDBd8CkVoqfL4gWkwkhk9PksFQyQaRTNA1VEJEthJJN+cR31lipdLLWSHOqnAJ7j8la/DrYexWwDh4JyIS/gAwXMH4cf+F8MkxxN4sWsUSmAXd1D+sI6FPWtN9uxvYsfk66AZ/FXZlX808/nWf77ENfJeX4XwBbedQo1lMcp2Ye2E4cmVVYPuGoLlBJOT7lEvS/Pvz3Upq9/iV/Zx7mHEOLxKrOycGc2ShOa+3aMXwW5nxOyn0aZnw18yqd0732e/wtwI8q5ENZMeUwGSmLxJTNczDx6SKZQxm+Fms0Ts2Yy+Xyubz+Kl9QRxeiys11rs5MuWI5/0z73qSvpyk77w/vO9u+mErube8xq+C9Whvs8DZ2+pLx5pMtM8nIzpzxPUWmNV1nZO12bge5F3WjCVjL+TrAAaHRQ/Rcg4Vo9J0dYGdyI+71Zwb12YLcOGQ0EA3IAR3lyroy0C35w0hhdsBE9HpuCTYo6KnNVg/to2Bca7BhHr3dTk3UNgSY6Q2cfuWinbWAqS1UJnBIu7J9L57XzSOdjl9i3ScXwMarEkWExAlxfGgAgJEPBJoCvkDAxLtLtJnGQLCIOlsdzqyTZyZc60bmlGxldoGXqtHMzqOPHUncf/7YVV+54siDR5NTsWinvm62JT7c3HO8vXXQYm4vi4Tq69oDi5/Zu/zFlf137csfzlra39Ef3GMg7en4zkRP6uqjDx65/CtXHvzs0sRlGbBDE5GJZP/Jvpi3V9v6T+6GQHihd/Zj8yvnlvd+ZrHG4/Y3vLY8aitL5zMLqbYBduYN8PZ14Ps8SPKwGDJgAMwIYCCgpAakOBWrYFRQjUqv15fpy2QVuQqkOM9M2qARk6x07eAAjtOfw/fff8895Nzq1Kv4jHQGgL4fxj8G41eCptWIusR24Pwwgw6oDuSjdhnOVnEQ8VjWxOlc7mqL4KmvbnQ3VjkEl8UVadAzRXijAPBgquqAzmO3Kr8I+3Frdi6RjLf3pRY7pK/hUMfoeNdPftM3N9f3G3IuMtGSHndWL7Zl5hL41r50qu/X0qPjHR1j0m8pH6Hy6wNAp7UoJoarXcYyiiQjKrt2bHBhwYNaVBPwB6gLS7N+/nzpmePsvQcO3Ls4cKYlErokN37LxMQt47lLQpGWMwMmOLmlew9mWyNNian3Tk6+byrRHG1pg3OhcHuE6Vd2ZrUUwMQMcgVMJqPNYrSb7IEG6tuxqlgZxlkF/ShEfn7JfYuL913y85/P3zY+ftv8/eTc7r8+fPgzuzuG37Nr103Dq88x+T8J88VgPiOKixGVK1JdSrMIoqu8WBtmVoZR9niBamWl/FGQX3fg66T34TslD/4Rueq1vCR1kXNdhfHTML4BNYkBdXzK6dioqooBXxiQgY7MeB4b2cvGPSXd0oP/nQ36VXVMel53wnl5UFpsBa0LcfWEaLRMiddqEEMuZgM7qCbKWCxoG6ARBryymecRNhLvpsNLe/EHsLXljuFFdoDDd8ABXt8lH2D6RNN38GHpv+M5+QjbElH1CP2e86iw56sYTIOiT1FXl0FHpvulfqkKtBmWmO4WXkl4J1eJ0qIo4nspOeFuum3pJdwqj4s+zHysVurAWLd5ZHMRDJ5JUaS9qJ0EsucZaCtQLGLmBcNopiyTJQ6QycnALqBKm18D3Ys0ZVgGJbMHTzZEnXtbwv0BEawzU3cyHs1Fdrbif5RifZd0whz7YKgr2D4bxFqDXkuYdMPM88bJnlOrxSLL06SAkwagCRBd+0R86az0Kk5NS7+7GtYrnQSd+znp/Xjk1EsMfv0wLoFxtVQnZYumozE6kEGnRVpBoIv2A54kBUKkXrELUG71Q6w/paFfrK+L32JdNkFdlwGMEq9ggBPYL2Id1omi9Lr0Ol2WDf/H6hQJss9/Vsf9S4bLdaKbJ6RkVEthVAzL8spjPg4jDtFjeYIMwniDq09Q/ktx+Hf/P+xqmensv/vAgbv3j988MXHzuIy4Css5eO/S0mcPdk69b3LyvVMy3jJ5QHH2IOzNBHwHLGNYBZAqh6mbZN2RD1ssN9ssZnu5XQg06KgT31PgPXavypaFSVw9cGVPz5UD/0fEFfMnTsy/TM61Hc6DHJNwx8zg4Kz0fDEMbGBV5sQM9atriQ7MPOaB5DCHh9fNp2Lftt1uD9mDsUCQMmGQ5E5+gzSGFZFsMOvcJIrJ7yJNY00741d2LaowWpu4pum6Bl8BSLjq2kRvYkcgXACW9P38cuJY8FCqGFxF8CqH9YD8JNQBp5qmy6CKKOxNIXefzyMIVsp7YLVWbxBsYIFBTptUYEYOXjb/6MjDz4kMdFLsZQY2fMvV5RL8Y+D7NIWc7Ed8jUTIUyhI5ZXDTu1iQrVKFVTUyVskr4Io4Iv4KKgojSsqSiCoQmud+zmcCk798v3p0JFb0xNNe49efVnHSs8tJ0Lh49nYUNOeo1demTsxYsylWo94O72Zrir39Hh2T/JQa3Pc2+VraXO5d+/MLiRlPhgFGA0zHUP21RTMStm5oqja2At2pReoxf0rUvMrEczKLoWO5wA/zkJ/O/KIdYpDH+PirdmRzeazqaKY8q2NLAzXnhkUswzzxMEzU6aRW2bwJ6WVvuMdHcf76G8zt4zIa1X1IR2dS8vsYOCaCsOUuQ6o/DLXoXwM+Ab5uvQPA/CDTdhEuS687iIHYCw/mEkaRkswVhnl+8AoaFiJiXG2eeAUlkotlaJJN6Y/nNeIvf6hbzwz+JWvD/285xvf7IHhniL97DVFdq9+Xl4nyFByM+NtwBsNOhgajp6yn/JRLOOaxWIRKFxhlTCkgY2OT2Hd5I9+NIl56b8nf/TqJJ6X7sc+6Qd4Hu+Gd588tgXGPgNj61GtWK3jgAgLDE7xBFgE6glQxoQjC0m/2PnTn09I/68PV+G/kj4Pox2XfkbHaoexRFXuU3iuu9kKvoWCm80isBVnZRcDCP92/IC0F78hTeMLq68nSXdXcvWrsuyfWvsgbiM/fJtWXBIQC15TPz137qfkh62rVup7XXtj7VF81zY+UA50W+0AXpJdoBi1wHxVhflk/9wyomoZ9dfS+dD6fE5g+ml4tcB0P+0hv2x98xwdPoM/gx+UceuC7ki/6ADuzryWMDP14F6teFgu6FC/NQnsgfd+L3NX+00350AK/erVV+mapbV3kl1r52G6BjbGNj5jOgQPACRkZvWBnTn5XLuJiN4kL0NfJ+trgp5D8M3VFhDH8qxZp7e798Ys+UrFh2X+1gr6wy+IgKoB43aK41WY0zhBIAigxdfVajmdVkPNWK2OaGVvJdXiXYxWRvVYp1OVLjeYmO6A2++ph5FcPr/XagA4IYcdAFXs6mK2EbVwLaB+qeEwfPpE3n0iu+uSzGL7xNLAzoFp1/4F1yXls5M9uyc6iHDNQekbuyKte0daJyL19r59sWSr5M63TVf3tCa75Jg0yYN8sYAF0i12VpQRrYbxycrRglvGxXSoDc45qxUha6PV43ZBT2Dbm5xzQYpTQrFPThW9HzuRz5/o7T5cn8/XH+4OzSQSM8n0dCw2nSbC4I1jY6cGO9PL5Enp39KdUk3boc7Og23U7X0wC2eUAHj/BuC9tQ3i2t4GcbylDfLbGwYHbxhKLvrD7qFgdl86vS8bGnaH/ftTpqF3jYycGgr5mmsbcgc7Og7lPHXN/iZ69hmAm2cdbgLH4EahxQDHVGvlrHnmz1MU7GK4WQJegcINKwe9vkqBbJbLHgquXhV4NzGIJRn0yJOrO5bTnYOnxsZuBOBh9+pVWQqxTNvBzs5D1F4CuBEfwM2J6qiVCTyGI0wQE05DlgvLc43qivSHqiqEquqqat3V8JcDDAO6Up8MsWItwlNHkkTHe2Cx+MgBd7Dh+vb+6wZ3XLNj7B0d0qg2M5uauLQMX6s7MBn11TZ7o0OnxkZvHBy+dSE934L/dnnn1BFGfyAP8BR5BaTVHrGsAuu0lRjpyIjsDKlBOp12CTSGKmZwo0XQfmTHG8tQqAZlVwd0tlL65YJY5vWy2BXPu2Ve56X8hzmGmPNYZ3/s1lvzMzP9qVSkwRGo9hFtTkrhb+cG20c8MUezR6b1+NoMqQEY0lj8kLijrpLotIofE2gZGJ6OrFD6holBsaFuGVfBmWlFYw0gO+VwOIxQH/D7fZTGsaCQC7MqMhu8mYAF6RIsqOm+cqjzSl9Nw2LLwrG6FbH30q6uS3t7jtbdOptIzKaT0/H4dJJopdbelfZAfWutZ+/k/nS7eMXAjivEXPqQtCcxlwX7vmUuDdBncB+Htz8AHttppMIi+7YZDoOyxrxtriJzDFQJwea1UfuT0jqAUnHXCAq548sWWvIT+ehIOJ+3L7YRoXVPTnoE9/dMB3qD0mNA16+FMwye3fD+cfIVkNEV1AIsuFOr6MEJo6o1XVFuNjF3qXaTu5QHEbLb5bJY4AW8hFzisgjV1YLFtbz6Boy/9tTaBPoIG99Ncb4C+HI51sBpaYtm4jiGJxrgchqNWzPGXLRuU7XTYRXYrHyJk1Y5KqY86bwzygreEa0oM/sFtzffn1pfyps/MfBpLd9UTzKrL7QNM3gDSwDqexJG94oNBg3QG6eyW7oaVT8RrEzaYyroGC8F2Fb/YOLLy/k8bprCVdLPfnX0fQDOWhyV5Rc9nvfDuKrdW1li944BlIBVyG25IOCyF6XEloZq2e7VagVGXLolMAZ1Oteohsb95HP3okavYPVavTYDUFHR0es2/JK0y3gL7+S+6Vh+qE2cyg/St7x1Kt0+Z6/c31mEEhP59U+iHWuKd6ZAJBVwUtiEk8KfgZP2t4GT2lmGkgqfHIR5N9mbrovam463tjdfOzU8fGpw8Mbh4RsHM4vZ7GImS9+zppF3DQ2dohIG5Eyu/XAud6i9/VCu/VC7vJ5p4Dl5WE+JbBaKZfO6WKYAsuKLymYVMH+WbJZ+TS4sl8hmKg9nQB4KW8hDoUgerovCUZkvbicPhYtxwreQh9rVSSysC8RlKbRBHmI0Dfi1AGs10Sw8OTqr4te6JgHCQ7DJOrclk7QXMOpvTgy0700CGb7Sk4tPp6UfEe0l1I4D3eRpGDOwpb3p2mxvBpDfFy62NzPF5qZDkUzU2iTs2L59VdSzbyE92DewfzI+1Ro/0Fy/c6Slr2tndjDcMpsyNflivS3+YNTu7ss09/rr3YmWqK+xqVrw5cLh/oDMI/ywxinyUZDncTHixDq2b8LdTFkiWaTOU4CBTqdqAExuWhup4PTLeqig5H0AR6Ju/EwWT9laq1P9MzP597zHV22pN9orhZF2PJP70Idy0gOeZlMZ400w738RrUzPHM0uGdECd6GzUf0IUeJCapKGHdm9Ni/zcRbpRZTdyla5gP+LknGbStRw3G8ABQNR4zHpKUbUeFjeL+jVxEa0ah6EaqmpuoFgYRaFVTYt3Tsef2zHq1kQniP4USpDMLA9xFmhf6mNKry1jertve0vMh95f++D+Q9+KA8jTuCH6Gv1DXy/NF+wpfEfYWyWd1PGa7CGsjlq6cDYHClygIKtSvHEAxaPNZh0ZpO8FZ+7997hb31l+NOfHn7mue9/H+tXX3xxVfojHbdubYy4YFyBwtqoJ7BkjMEAVoYuoKGbUzybFpuXoaGMhd2YY7sox/xhf2Wtt7apvP5f+5/5Ut+vqsayjwjZCqerj5ilLvzs6pOdWSzvBdgnfgnm3MaOFd7ajs3gPulZfIf0FB6QjrXgT3a0SCsdbNzw2h68lzwBHAVg5GJyoBLjoUaWVQQN5hWCxWhScFAxp4nhLgzsP011KZq96SwndqccFOFpGJzHpll/ItsW98+OaDvyLuzzB7zYle/Q3hnqT38wFW2JpW7P9Af1cX1NovmOeNZkziQ+HE7U6OMwy2Vrj6K7t7GJqYS9LJVSkoKo/2kP3s/WHhL9embLCmBRNmI8uJ5Wt0RBNUmzZQlvCztBbATlgH6WxuqzdaQWA9bbG3XwCScSYysOBHxsxSOz/nhbNgE7eScsNvzheNZsysbvaKaL1Qf7M7enYi3R1AfT/SH92hrqwc34NP6CwGPzmiT9EhkuIPy49EsWJaayZtfaHPoiEVR9jK2OCjzXqJpDYiUl+hjTieRkLtAOkh9V4qm+YXOCCGr8dHUH/pUqXx8F+20c1QBlAZdsqK9xVzltVrO2TFaC1BRdWXlmPEioltGFhsu96RhRc3LpWdKkXHrGPrCXgDvjl2ZjYv5ILnckL8amG8NV+UZvd1VYumm+r2++IcR19xrHrurtvWrUKHZxQU9zdT0nzWsaqpuvPingu4WTcu5UFhYaYXGuPlGEo6VJGoinuZ48HtZiAkyTJ8zly5R8mq3B80Vu91pUK9j8PtDfqKrkt3vSWZYtt9H8rMU0x4dEJGNHLgcydMdJV7xyIQec+sUXu7rq617M3d5/olNMRWNt0ank7bkXN/ieHNQTbMPIgMH2QDzLNjiqwwTUFA3hlstYWracX+SwWy1qCnalkSZhs3R4u5rEBy9G8Pi2h1555ZU+eD30R+qtwv25PbnrroM3fCl1WbHz6yd78Bx5gOVBtDIvSZAmCgIyM8flkpajwMKTW2Q6UAdKsSLdWvT7vqoqodJVJZxTPske+umqpL/Ln4C/YZTGX8Z/V9mIw1pUyeEwelLx28zia8jZt+PzofkK3TgjvUDOtrxdn4+T9z7RdzZLXq44I/O64NpLgMPn4LRBf0dUfCJymorO91Axxtx2zG0bYFIBKzpMB1VpqP/W2L+Sazvc9dv0jWkcbtmdy+1uWa0nX1zdJedi/gR9CncA0OpEdxGXIJR9TBYlMGaKoLdYXc1MkJ+4rPTD6lL8WWsieo1mQaJr2d4qnTbAZasBxJee8mewrE1gWTeAOg3yWHMFzbYArr2yzlgP0GF2ukUPawKYdXq7NgsizSV1IZfV5/NRwU1Bx6LRzLZVtBvquwLhlUm+FGwLVcUqLbU+R53daiuv9CSqNPqov8YXqzAHKRJYjcJEDrOahhSc+7Ps3CM8PfcIelrJxxjB/8z4lQ+1i9nGKqdJQ+TwAkF0rQwhZQYG0ucAUkSez+t22a0FvETFLExJjqJuIZ2qfQKzwSGFr3m2yCFZ53PSmS2ySNgemN+cu76yEdZK83T60EfRF5D+AsHn5TSdLdocx85t2pxU22AefXGbNiuFNmZ0xzZt5gpzHUWfk9uQzW2+URjHiL69sY2cH8E9xGjBggbFfoHyIib19QgDF9LDOeg1ywZgkLpF0CsVf7ZW1pyYGWWptEDvCoVBGYFjYjVinqZJFKAhgCVhUyPn0mv7R/DtRCd5fqaE0GlCRZf0DvwR6XKkxL1FlqeQQa+L7kw6GNDwOjdgajUNmbkqAI/LMeG0CvpHaCyKw1fAG9K8Q/UyOUYR8HTFpALpdwDW7SY0ycZGM3NoH4Q1p99uJ1pvE9vQiTt1sV5ieHMHmg1FjmzRD43RtBxjwN8M1GcFA6oGpGUFVvnDZgqUZehWrlCyeG1PuUZfu5k8q+Y/Pr9VjkY8y3nxRqpt0x/5zJ6SpA2KTyyHguF3SKGB9zN8wkV4ubnNcfTwNm1Oqm2ABm7cps1cYZyj6Cq5zTp+r1Et7cNsroi8nrU7thoHVxa1OY5sm9us/QLGeY2tJyKvZ+1vS9r8O7T5I1uPPM7RtXs2rgdoqRneXmCx0lqazbtR01jUYxooNRSpGhUV8FFbUcNKrezQzQwyp2zd0GfGU1JQjXygqyQP5it+Tk54GRlRU16+g/+xkPaCu/O4dfV2OfnlD3lWigRwYDF9xlNaFJ7yiRJYsTg1g1Wrcr6PlfCdzW2O49w2bU6qbeB8X9mmzUqhjRk9tE2bucJcR9Fzm/gXRrvQX+KvER0IA93DepobCUplEEgkmHVmnThze+R2+eeDYTyh/nb77WGk5vP+juXI+1Cc1gdFwrU1LofJoGc+Gpa2o7g9HHL4RFcUPvH7/XF/LGgNWlkGtprVGgAbL1uUL5fkkcOJFTJFckgaU+/HM6kDd+9vvzSWHp6LZ4A2209E08Ozq/8W8uNT/vkY0Cg+cfNEyCfdAn+RmnfvWPrswYC361DLmR1AnfQ36TsrIfxwTT0QqfT9qfdNZo82SeM19Qx2LKbMzqlNOcsnSs57c5vj6P9u0+ak2gbO8gvbtJkrjHMU3b2ZVmW9l83Vqcz19MZxNuUaxBj3hHPQLuuwEuUoFK2B5Klw2CqclU4h4Knk5QhosihDw1/I0Oj555IMjVMsRaPtpmdmBgdnpBdkmTPDcnGeBRpuEWNGUMKZGgICh1tREy6ZWxsv6uR0QavP4/dEvCwkUZK7Hcbp9YIFlXFT/+pMel8uty/VGmnrSe3J7hkI7wjN9e3o6BifbG+fFIk5OZ1ITCdTM1Xu/dn0fEuHrzvYMdoxkm4bHc+tSgBHOV75MsCxH2QvQX2Xy/S98TkH8P1E0fPn1faYP1Dc/unCc3Nv8fMHCuMfnS96zrkL7Y1UOwMagufcXaDDRUHL60HHxOUagJ3HDUqCHetJDuvKDKB48jotDaOW6UjZCtIjHa/XLZsNRM0Gdo2WG00cFYbUtwO0lU7HYgile9JiZ3ssFUu2JGCCiNXn9fl9/goAuRrEktNTS6KrsgN7U2gL0VxzNeZKXpXjXH3XeuuuGdp7aXHodfCAw3t5T0nsS7plIkpDspP9chRsoKNjYGFsPSbblc3ki2Ni0kx4NBqo7mlNdco4llgTWewzgy6I7kTc06DRauxYp02BWq8rqPWqXuPn4SuCdVeg4hT7QlCKnqMbqbpJgDXW6kBpfsvWore0IbUE0JFCew2tAgXJBMvMyAqJnq8Oa/5ERYQW0WWS+HCJItJzaddWkdrGuUR5iQGR049es6MkeNsY0jRiipNyTJTi/JBMCzfIuLrxOaWFe4ueP6+2x/ylxe0fKIxz9BB7vkZZw21snJfk8T8gtx8FgBmLnh+vkNv/G3z8ho3/kjz+ffLzH8PH79j4cvujn1mvq2glfw/aQhTtEa2VzKtaC+oAaAM2q6UMD8l55w7VBS1syA5zizaapsFhslL8GCy5urq6aF0k4Av4ad6sKpkKiVABKiDpSdJs7s1eefxPi+KVw8NX9XSd6D/WFz58ynmwPtsVDB9yjlTMxmOzbZnZeGIuQyxfOLDz9EDvtaPDJ3tmZuazqXC1r7rGG0l5Vl9I7mtv25NK7sm1700BvORYEOU1UzKvGVmH+yCD4y52Tn3ovVs+P47OFz1/XnkO8L2ueJynC8/Ne4qfP6A+R0cvl3lWHzpNykkd85O50SG5wLqGVitQ1wvS8Fir0R7WqUFGFy2kbURIp5aEqO20Ws08kI12iXWYXBAdzCByV1aDcFKdbHpqoRYHPcEuKi5qwA7FMv0pLb/4jlrO8DXVJJ3NX1ivYuAK8S0nWG7NrAYJLPtQsNrlNOq0Gj3GWk4JhKwnMGzWXDweT7OnyW8JWjZoLsWKC9NbnFiOwmkUrQW4ZkNw+MbB6f6G4W5vaPjU4K7BhmFROtmCTal8djGL8WLW5ZReS+bxx/enht410uIfDhxIDZ0aaQ2MSm/mcXug/VDuu+2Hc4GBGuk5P5yRHFegZ71bpjGFJjc+pzjwsaLnz6vtMb+vuP0DhXGOTsvPZV86HWefMs7Hi+JyF8uZ+bPicsKfH5fjVrZIminaxwPKPmB/M6jIxk8U/ADHFL2+WI+WZfrZgkw/NlLcd2/BP2BQ9LbSvhcKct8wLfOuhrVpzgq6mhNVU/i5sI7TY4KdIFG4EZAxiBo/R5kOzS6XGFW1KCtNS6mqrqr2CYLg8FDZolW8N9mgXHQhS+jWrJFwVm9LvMMu9BcqMGoaPbUN1h/cf/8dtbmEs8n0KVaN0eiubbDjHawmg+Z0TZMc0EgziLBFcQ/VSNyYKiQ8iUUNnJ7XY62+mSZBU6WE1xO+UInqouusACtNr1djnEowDdYdDocz4XTAZwU1JOQpAyO9sPL0NmqIXVA3EyhK+CI5ui23oyTp6/AJeX8/V5O/6C7dUWPfxuwvmhDGtvyRQhaYai99gOnYAUXH/kOJri7nX1F6GFDo4ZNFfU+qfbEePbNN3+eVvgTrD67blo+wvkGl780leERzrb5GnoQ2g6xNBWjxFmSANo9zFtoI2ij2M2Dou5Gan3UIaLQavhkQ+ywaYtDTakstTZzWomU4mspCBZXC56jjnPE9Jxlzu93N7qYgjYAGvIrr3FuSp6UWwqPN4elDPRvi0+fP53vWUHGQenT1S0VJW38b6+1dfWRDmLrgx0gU/BjHYNlbwBX0j7MF/ePYLNrKB4LL0Pe26avqLhy0UepdgAZovUsN9epVgowQMK3XVTNJKYLzi4jnFYceZXcVo0yYAQFYWX1xDaoRbF6a0S1QXFfrYLwqRjtl5M8WFcZMyuhbQ9G7Rnq2UCXDkDUMuByqYcUtNI50mOWz0RtfzohWvw/4bzkGxRQ0Gh4Erwa0YA+tuUM6ELQ6zTKN0agHrVz9wBIQGJlaqYj2q00ZAPDhrXrQbLJKdmVMzOW1BvxeLw3xaDZuSFeSHCdrtLyaI3d9YZMGW9mmTLkWsCtsYuBvFKOhslbZucNGitPmwIJweMvw91jy3BY+Mf0WPrHN+qtekZWsTojx8WaFjz9b0lfOfaN8fETm4ytKX+nXtMZI7gv4U0GMTEdi7TnqlIqhPDosHojWEz1f5aSGOK2DR0NG6hzm9JoVg6oolQP16XQCdWsyNJLjjkty3DEep+6ueD7elU1XxCpi4Savp6babqVuryqTqiTRq1Rkvcj5J+bVYberxlsXMAhOp+WXbyvFbq+nurot6giH6OUnc2833w70+f8N+FXGaFr2BR5b+3iJv/BlaKNlNC3r/MfuUHIG1qbRD4EurTTOTiWmVY6zq2XoFGSqvPGBfGTp1qqUAd1CJb4fUuyrtosywVExUR0xkbsVUchk9NobMNcTLNbnoTeq2G1Ew3mwXLZfODJVWawoEAkLEjfUgRXiqDBvmUJnVRfkpOU/6pIWqNVWVVU5SZfmdVvy9fLi1FigULXqgHXWt+h5/MfVN5SFAg7SSs2/WvetAg/jN/mGWM4d2GLaQm6cUJIbN5rP0zwSGG8f4PQVIEcaAXcZTuNvyXYbPOcYrk8oz29jz1kdFpObcUVulpXQD4AEl3O3QptJpY0FPSq3eVRtI9dzPamOA+NfxWQcLpJxdJwwazOptLluYxs5j5Z0AY6YaQabyVhmAM2e0xO1sm1TkZgZme2FIjE+TZEka+cF0iX17tkjfuQjXV24LibG8JT0yqg4KkmokKuLWQ5jg1hrLON1VKjqleq5SqUYxM4ue8E8aLXKwBhP5cfH81P4QLP0AnaGxBA+Ij3ZXOxjvl71MQOMflUCR9kWfFmxBan+cZfcF87mL9mZtShn83u5vfRrmkeotofn9xXNdbLgz9bDaWw91/MFu1N/DBX5lRMFv/Ix9OUSfUW2s84WbMdjh9BW/m9cjv6xJO620X4luPy0rDOngLE+C+dqAYu/XcwWqlD1pFCGaqDldmqeDTM2aq01DhtLrJMFsWs9vKayQxphW1db/LIoXv2H/fccOHDPfhKXPDvlgI8ijHd/ev/S3fu7Vl8g2YlbJyfePaTYldwbLJ80idrRlDjhxHoddd0xrQE4vVavWTYZiE4nV1W6Ro18GUd1esVll0r5wIpItady2Ywv6WuNNNOUU6sv4PObYdEbHXbrfLxIyDZs4uhIlrZch+ypG7qh0fuukUGWhzn8Lq/nuqGCzJWOFWVk4jMbctXHe3sm5BzN8W5xTBa+xWmaav66IoMPsrNNK/j7TAleyHmH9GxnZd/PmKKrAf4eZPibVvD0Cbk94O8Cw99Z5bmxaK6T6lyAv+/dcq79DH9nZfy1yXOxuji2zoyyzrtKcF/OZaTrnJPXqcRWY2siq6kT0RuiM9naUK/R6d006l5dCaKdplhrFP9jdGNcFfTFJRDsDtkg0uKtA6uxTYHVi/ei3sv45kDpxbrJQd8tQqtbdJRjqyZfxEdTG6wWA5gBPtk9uU10FV+8epBE0kd2VGwVaB28PrF9TeHN86mSeGtO2xZ/iypDlpcqsrzUPJxZTWtLfZ1Gy2+MhWtG3KDxDm8ZEOd53RKiyeBo3Zdx0YD4RTttGRC/SK9tA+Kl/ZSAuC8censBcXyxBFz89KGRLQPjiaXAtlm5o7u0jSWnFazfPk23mD5PqvQJtH1sG/p8XqVPrM8qtaRAn7SW1IMeFu1VcLTIaQaJYKKXZ414sEY95CaEaIrpFSxERU5RR5WaoMF0OULwAZDkbqweVjPtoEHc6bfRQwxuakwLAgk+sqkPixTQ4n5GWPSM2I1TW54Qtbtp+SuetW5xCjVltCY2tgnWfUU1sn6gAZr3HEfPia4mrOea7cSgj9oIMZRETiJIq9HeXIZpbY/hChBeAF5a2cNxmkWdXN9TEkCJrvfRG8jpi3UCcG7ZXgmmFHXjKJAs0C2O4iwb2wnAoq6IbSIquDRD28kwHwdKcDeSXc/abmjwV2wRQBlMFmdyx6p0fjkni9UsMxzNKjj6zhIcpbnmP2C66rysq5KmEn1Wjv0mCrHfY+hjpboz882eLfhmjw2gorjxNwqxZSP6VkncmPVlcUfZ32u8AW0VuwY97PNb9QWLv+BbxuVLSKm3nmb11n7UIbY5gCWVw8mC6MPciHJ7hXpHKnP7rhtF0MPvq/L6ffJdFrLXfJM3ZFN9Njk7eGbKrKsqeAyqDlytVGqTc6wuG1saFBeBv9rxyRPFddss736a7AC7h63VALaiHms5P6a3EinJ/tRfWUi/L3ZX+mhNTchDI9Qb/ZQbc/HXbUqyQ3ZLFqfm59cNTHfUOLA5Sb9gxjF8ofXgDKfa5VwBPFWCC3I+PuV7e5g+xKPvFeUHnFTzA+D5rSV9ZR/486oPHPOLqKjvXCG34Cg6VYILrE6b4UuXojM9VDK+nANP8WVR1pkUXJP7zql9wT797jZ9H1D6Uj/cnUV9Txb66tEN2/R9Xp0X63sZnrL6t1vh7Cuor4Bm5dK6BeYrENavN3EzxKxA5VZ2vYluU3Wbvqiq7aHNpWxy/iweIc+/zZptJ63Z/lzXsfTUzjR5/rbFxa3HKNRpEHQz0M7VSpWGMkY2yXu70zun0se6yPOLi7cpY4zjw+QCvW2RjdFI8wY01NFVmkJrRiY/V0hIVvbLzIv7x+pCjmxtbdYRqh1rIuN1dSG702kP1dbLc+xBdzB/SIDN8Zb5uZuznecV78a6N0OugYR1mzk3y6duY6PSYj6aUbyixRzG3Hyh8Ix7W1nVxfck39jZGQ7Dq9rnq3b5/S4yLv8d7mzyu+SHcp7wNFpFNiSgdraCuMrPaKUkAyGtcgD5Ps+4GUe/mZQrR3wOjZJZrdztW+TcvaHD624pOHQfZJ4bm+LJle+dJV34DHkadt7L5s3aAIZ65s+yYjwEQpDVeBCMVtjNBfNqcTfhJpmS5wBRqFUQA1HNTb14lOrYdBH0agtzvUtwmEOmZj4RtLPfg2b6O+mzWCvKh/hsp/opn0c3fpzlg/Oomq2qQgvYOESt1KutlkIeuZP3+pXEcJztPZshL1fc9GGWII7X5qRfrz28dj8qRz42gqt8i2oVX2EsfuNluF+QfZA2V433Ddm3yPyMBf54vcofUR/ObMMfX1b5I+pDTxXx1vW+x/GXL9r3OBaL+s4V+h7FQyX8Ue77QKHvUeq3ZPcTLtA4QnEM4s01WQd4c03RARJru6kvX/ZLszbffFOSYztvSkob2b+9UhjHDLxua//20wX/tnkcbZk71reFjrExzktQ3wnlflX0n8SAoxe/f/gfpDiOdqh9uPa30Ycjb0pqn2H8bXSB3Ak40/owoMvQpsu4XZsu45arhhYekbHIqjq9ZSF9gV6nXG2n1ymTW+l9yha3fJ8yhSP+FnqMfAqgUIGoLGA1QGSkUAME68An2TpqUfsjteVEXYqZFdaz++LZYqwbyj44xv4WHvVV+SzsRpGNK3Juu8B/3WatrtJ14+vIpyqD8rrZ+r8K67cU1k9RSt0IvXPrcdyNP/2n1VH8r76+eLy3N/64/BHvk/FnGPAqhHYCbgiAG154/yrDcQHfDA14ek+/ZnfRPf3daBRm+w/5rv6WCqwtl6/R12OdCZcZdWXFV+9bzRbOaOSWBEMlrym+sT99kY7s9n25N097c8q9/bkt+21xc39JXzA0MmNjoqje4D82Pza3a6c4Ko4M9Ce6E/m2zJa3+dv+jNv8Gzb97Stq25D5k2/6x8ODwcIf0hPqvf935/7U/wHAlv8zgPX/KQC9VyiN1vDf0dsmHtZinAh3YyfALnBv5hOfyLz+ddNjT5iVO53SoGQr7TjWLph18tFPfCL92c/2PvGY6evfYLLiZ8qdinE0KY7VuIlGB+LPiOnNuBoqCjXcsgGTMoyNNA+8kPlvwkaj6ulgtmEsEm5uEix+sPkEq99MfVSFezqCYBBS0wI+8qSQbKejmUQAZCd80PjXi/KVi2O3LTv2TXK6XYdcR24ZVhL+Zz7kxcPSZ3kNXpLO1390T4Jdw9h7cijndHrqc32XdrJs/wMTuVpvtS03e0KW7zhOyvFlwAd1D4NCl2BcT74p9jJ6RyxTdEDV4YArewQP4VZX6YvxoSTApU6pJdd/yWU1a0lii0tSSovC696qKPyN5W1rwoHO5TlBBuCEElt4gNG//PyC8nw99ik/f77wnL+6+PkD6nN09NLi5+vj94Fatf78bKH9sf1IgcE0uQxgQGMF+i81ea0YYFBiiqmQWLfDApthcplslxUDRrHL/rMIPoqNNlcEIsU8+4sNWQnra6Z2tbwXsL3/omgvTxdgYp6T5ShN+/kIu5tCxYWk4J1WrqOg36/NwPfa0u+1q5PofwAPfnx5AAABAAAAAQAAtCcAwl8PPPUAHwPoAAAAANPBnYYAAAAA1L6m9f9W/u8EWAPFAAAACAACAAAAAAAAeNpjYGRgYD767zYDA0vH/7D/k1kiGIAiyIDRGgClhgavAAAAeNqNlE1oE1EUhc+7k5ULwT8UBSlqElubpK2hDaY0lBRbbUrSjnYRakWhCxdaYrW6FtG6ExEXXfkDUvcuBbHuRMgmuNKK+EMUWlxkIS04nvuaqXXSgoHDNzO5b9675515poYz4M8MUQcoA9fcR788RFTOI+7sQEIeoBkf0W/G0EPFzQzSMoysAfJmCiks4oS56/2UJ0ibIvbKSbRLDw7LBFVASs6hW05zTAFJvbb1HMu6Ln0PmTM17HNKaJUvaJJHGJc51tbICdYVqSrvXyGPBV7v4hw3MSaH0OcMsIZ1TpT/30De8hZruHaZRkzeY1TfGWpGWJ4hIvewXa7jmLmAYa55hWw3n9EpBe+3SSMjXeiQK3BlN9rITnHRxp7DMkkfshhCBRm89V7INgziHXLOFHL6XK7ZelfHmKv0cBExM8lxWf6fYG9JHJQ97G0A+0VYcwdHzFZcJOPmJXrp+4ids0hPuEYziz6zxJrnyNh1jSOKD/Q8yfslJOnXqlcbyPlOqn/q3TphwSurf+QP6puzBS2+d0HJToxYqn/rpf7RZ+nAKevVBnLKpPbi/itUvDf0b5D8Sn2SS8yF711Qmgtl1vr7V+qf+qzUfnXOILV3nd+n5oj7Yvu9zT1VP3RNm1GzpvtdJ72qcL3qXRO5Qh7XPmwGmQPNoWZhjWcRNhHuvc6r/QVofWVva1xGMtTCeZlbzU4DmWXNUwOn6xnzqfujHm1C/QZsDnUP1b/6t6B5DFIzzmxmrB4z82UyR3VTr5nDX3wGb9R/Z5ANntbnZG7hVFfPGyxTTwHpRcq5jBTPBHsumHlynpylvyVe81wKzSBhWhGhYjLnVW0+HI4t4eh/iZmB+webP/UMeNpNwl1IGgEAAGDzv1NPO/W68+66X+9ueueddxERETJEQiQkYkj0ENFDREQPQ0JkxAjpIXyIiBgjImSEhIwYISN6kBgRwweJHiQiIiQiehgSMmTsZQ/j+wwGw/I/e4ZyD9KzbowbT4wPJoMJN1VNd2armTK/NU+bD8w1i9EyZ9mxPFqT1iXroy1p27Ed2s5s97aOPdWb6M32/gQgIAnkgBLQdlCOIceCY9vxzXHntDonnBvOlotzrbp2XXVXG2TANJgFD8BzsAl23QPuUfc7d9UDeqY8ZU+3L9VX7WtDHLQCfYaOvUbvmLfgvfFpvhlfxffk5/0J/3v/vr8JW+EJeB4+gk/hl/5Yf7G/jjgRBBlC0sgHpITaUR+6iObRIrqPHqPn6HWACjwH/mAejMEGsQSWwRaxPHaJ3WAvuAGHcA4fwqfwGn6Ft/AOARA4MU1UiO/ED6JB3BJPxOvAJjlIxsgUmSHnyRUyT26Q25Sd8lEUJVHDVJxKUxUaphk6Qo/QCXqSLtMn9AV9RbfoX0yMOWXqTJN5ZjoswOIsxxbYXbbEnrA1DuI+cUdclbvkrrn74ErwY7AYPOcRPsTH+BSf4Rf5PF/k9/kG3xVAISDwgibEhbQwJywLa8Km0BRnxGUxJxbELbEkfhVrYv3NYWgttBXaC4fCjfBD+FWySz6JlzQpLqWlL1JXBuWALMnDclKekRfknFyQd+WSfC13IoFIKpKNVCNtZUyZVTaVPaWsnCoXSlNpKR0VUHFVVEfUxH9m1ZxaUc+iQJSJjkcz0Yw2oc1pWe1Ba+tGfVQf16f0WX1JX9XX9YZ+qz/pvweBv0tAvSoAAAABAAABPABYAAoAPwAEAAIAKAA5AIsAAACDARYAAwABeNqFks1OwkAUhc8UJIDGKDEuGhd9AflTIepSw0ZQIwo7EhAEIlAtxYTX8Cn0Tfx5Ad24du3ahYfhtqDBkEk738y599y50wKI4QMBqGAEwCGfMStYXI3ZwDLqwgFk4AgHkcSD8AJMvAmHmPslHEZaxYQjMJXnuYhtVRFeQkndC69gTX0KryKqvoWfsG6EhJ+RNDaEXxA28sKviBrnY34PwDQqOICNGwx54jaaaMHlyR75pHnyFDuxUKNqMa6lY/rkIucus/rM7SGOAhrMc7STjY5E5X3HM+pNDKhUGZViRlKPfVzgCGUck2Z5bE55zKth/alS4sphTFuf0ZqqOq9SiXTJ2WbMqPMT5jc4j/Lq1KrkU+pDXd/l3v93M/JzudpDguPul7Otfbu+a5yazbWX05esJlWXuwN+CS8mwdmr2dVdTmomZnY4a2/Sc5lqDVc63/VvqyB3l9OqxZHRWpYnS2GX7y3s+P9KFteMa2h/R+495zsWccsO2lQcxnR+AGiigvcAAHjabZNXbBxVFIa/37F33TZO771Xx173xCkua8exYycucezESca7Y2fxehfGu3FsugQCHkDwwjPlCRC9CiR4QKJX0XsH0XmkB+/cCV4k7sN8/xmd858z994hC3edG2Ae/7NUm36QxQyyycGHn1zyyKeAQgLMpIhZzGYOc6fq57OAhSxiMUtYyjKWs4KVrGI1a1jLOtazgY1sYjNb2Mo2tlPMDkooJUgZ5VRQSRXV1LCTXdSymz3sZR911NNAIyGaaGY/LRyglTYO0k4HhzhMJ11008MRejlKH/0c4zgDnOAkp7C4nau4mpu5gTt4n+u5lqf5mDu5jbt5nme5h0HC3EiEF7F5jhd4lZd4mVf4liHe4DVe516G+YWbeJs3eYvTfM+PXMcFRBlhlBhxbiHBRVyIwxgpkpxhnO84yyQTXMylXMJj3MrlXMYVXMkP/MTjytIMZStHPvn5i785J5SrPOVLKlChApqpIs3SbM3hV37TXM3TfC3QQi3id97RYi3RUi3Tcq3gc77QSq3Saq3RWq3Tem3QRm3iPu7XZm3RVm3TdhVrh0r4gz/5kq9UqqDKVK4KVapK1arRTu1SrXZrj/ZqH0+oTvVqUCNf841CvMtnfMCHfMSnvMcnalKz9qtFB9SqNh1Uuzp0SIfVqS51q0dH1MsDPMgjPMpDPMw13KWjPMOTPKU+fla/jum4BnRCJ3VKlgYVVkS2hvx1o1bYScT9lqGvbtCxz9g+y4W/LjGciNsjfsvQ1xi20kkRg8apCivpD3kWtmF+KJJIWuGwHU/m2/9Kf8izsj2rkPGwXRQ2hxOjo5ZJLRzOCPwtnnvUY4vnEzUsbM2sHMkIfG1WOJW0fTGDNtMvZtBuXsZdFLZnesQzPdpNetyFv8ObIWEY6Didig9bTmo0ZqWSgURm5Os0HRzToTOzg5PZodN0cAy6TNWYC38qHi0prQx6LPN1m6SkmabHmyZlmNPjROPDOan0M9Dzn8lSmZG/x9vBlGFBbzjqhFOjQzH7bMF4hu7L0BPT2tdvZpx0kd8/fdqT06ednjhYVuWyLFjp6x12rKlrNW7QaxzGXeT1RqK2Y49Fx/LGz6t0XWmovtpjjccGj42+PmM04SL9NlhSEvRY5rHcY4XHSsNgU3Yo5STcoKKpIccqtmLJfMudxUj37qdlkTX92ek4YJ0f0CS63dOywPt9jDb7mtZ5Vvo0THIyGou4ybnW2NQeRWwnL2J76h+3ZbchAAAAeNpj8N7BcCIoYiMjY1/kBsadHAwcDMkFGxnYnTZJMjJogRibeTgYOSAsMTYwi8NpF7MDAyMDJ5DN6bSLAcpmZnDZqMLYERixwaEjYiNzistGNRBvF0cDAyOLQ0dySARISSQQbObjYOTR2sH4v3UDS+9GJgaXzawpbAwuLgD+HCVgAAAAAAFYmPZ3AAA=) - format('woff'); - font-weight: 200; - font-style: normal; + font-family: Metropolis; + src: url(data:application/font-woff;charset=utf-8;base64,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) + format("woff"); + font-weight: 200; + font-style: normal; } @font-face { - font-family: Metropolis; - src: url(data:application/font-woff;charset=utf-8;base64,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) - format('woff'); - font-weight: 400; - font-style: normal; + font-family: Metropolis; + src: url(data:application/font-woff;charset=utf-8;base64,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) + format("woff"); + font-weight: 400; + font-style: normal; } @font-face { - font-family: Metropolis; - src: url(data:application/font-woff;charset=utf-8;base64,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) - format('woff'); - font-weight: 500; - font-style: normal; + font-family: Metropolis; + src: url(data:application/font-woff;charset=utf-8;base64,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) + format("woff"); + font-weight: 500; + font-style: normal; } @font-face { - font-family: Metropolis; - src: url(data:application/font-woff;charset=utf-8;base64,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) - format('woff'); - font-weight: 600; - font-style: normal; + font-family: Metropolis; + src: url(data:application/font-woff;charset=utf-8;base64,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) + format("woff"); + font-weight: 600; + font-style: normal; } :root { - --clr-link-active-color: var(--clr-color-secondary-action-500); - --clr-link-color: var(--clr-color-action-600); - --clr-link-hover-color: var(--clr-color-action-600); - --clr-link-visited-color: hsl(238, 41%, 53%); - --clr-custom-links-hover-color: var(--clr-color-neutral-200); + --clr-link-active-color: var(--clr-color-secondary-action-500); + --clr-link-color: var(--clr-color-action-600); + --clr-link-hover-color: var(--clr-color-action-600); + --clr-link-visited-color: hsl(238, 41%, 53%); + --clr-custom-links-hover-color: var(--clr-color-neutral-200); } html { - -webkit-box-sizing: border-box; - box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; } *, ::after, ::before { - -webkit-box-sizing: inherit; - box-sizing: inherit; + -webkit-box-sizing: inherit; + box-sizing: inherit; } @-ms-viewport { - width: device-width; + width: device-width; } html { - -ms-overflow-style: scrollbar; - -webkit-tap-highlight-color: transparent; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: transparent; } img { - vertical-align: middle; + vertical-align: middle; } -[role='button'] { - cursor: pointer; +[role="button"] { + cursor: pointer; } -[role='button'], +[role="button"], a, area, button, @@ -3221,358 +3221,347 @@ label, select, summary, textarea { - -ms-touch-action: manipulation; - touch-action: manipulation; + -ms-touch-action: manipulation; + touch-action: manipulation; } button:focus { - outline: 1px dotted; - outline: 5px auto -webkit-focus-ring-color; + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; } button, input, select, textarea { - border-radius: 0; + border-radius: 0; } -input[type='checkbox']:disabled, -input[type='radio']:disabled { - cursor: not-allowed; +input[type="checkbox"]:disabled, +input[type="radio"]:disabled { + cursor: not-allowed; } -input[type='date'], -input[type='datetime-local'], -input[type='month'], -input[type='time'] { - -webkit-appearance: listbox; +input[type="date"], +input[type="datetime-local"], +input[type="month"], +input[type="time"] { + -webkit-appearance: listbox; } textarea { - resize: vertical; + resize: vertical; } fieldset { - min-width: 0; - padding: 0; - margin: 0; - border: 0; + min-width: 0; + padding: 0; + margin: 0; + border: 0; } legend { - display: block; - width: 100%; - padding: 0; - line-height: inherit; + display: block; + width: 100%; + padding: 0; + line-height: inherit; } -input[type='search'] { - -webkit-appearance: none; +input[type="search"] { + -webkit-appearance: none; } [hidden] { - display: none !important; + display: none !important; } dl { - margin-bottom: 0; - margin-top: 1rem; + margin-bottom: 0; + margin-top: 1rem; } table { - border-spacing: 0; + border-spacing: 0; } a:link { - color: #0072a3; - color: var(--clr-link-color, #0072a3); - text-decoration: none; + color: #0072a3; + color: var(--clr-link-color, #0072a3); + text-decoration: none; } a:hover { - color: #0072a3; - color: var(--clr-link-hover-color, #0072a3); - text-decoration: underline; + color: #0072a3; + color: var(--clr-link-hover-color, #0072a3); + text-decoration: underline; } a:active { - color: #9e57bc; - color: var(--clr-link-active-color, #9e57bc); - text-decoration: underline; + color: #9e57bc; + color: var(--clr-link-active-color, #9e57bc); + text-decoration: underline; } a:visited { - color: #5659b8; - color: var(--clr-link-visited-color, #5659b8); - text-decoration: none; + color: #5659b8; + color: var(--clr-link-visited-color, #5659b8); + text-decoration: none; } .clr-sr-only { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); - clip-path: inset(50%); - padding: 0; - border: 0; - height: 1px; - width: 1px; - overflow: hidden; - white-space: nowrap; - top: 0; - left: 0; + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; + white-space: nowrap; + top: 0; + left: 0; } .alert-icon, .clr-icon { - display: inline-block; - height: 0.8rem; - width: 0.8rem; - padding: 0; - background-repeat: no-repeat; - background-size: contain; - vertical-align: middle; + display: inline-block; + height: 0.8rem; + width: 0.8rem; + padding: 0; + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; } .alert-icon.clr-icon-warning, .alert-icon.icon-warning, .clr-icon.clr-icon-warning, .clr-icon.icon-warning { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.clr-i-outline%7Bfill-rule%3Aevenodd%3Bclip-rule%3Aevenodd%3Bfill%3A%23747474%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-triangle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C21.32a1.3%2C1.3%2C0%2C0%2C0%2C1.3-1.3V14a1.3%2C1.3%2C0%2C1%2C0-2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C0%2C18%2C21.32Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20cx%3D%2217.95%22%20cy%3D%2224.27%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20d%3D%22M30.33%2C25.54%2C20.59%2C7.6a3%2C3%2C0%2C0%2C0-5.27%2C0L5.57%2C25.54A3%2C3%2C0%2C0%2C0%2C8.21%2C30H27.69a3%2C3%2C0%2C0%2C0%2C2.64-4.43Zm-1.78%2C1.94a1%2C1%2C0%2C0%2C1-.86.49H8.21a1%2C1%2C0%2C0%2C1-.88-1.48L17.07%2C8.55a1%2C1%2C0%2C0%2C1%2C1.76%2C0l9.74%2C17.94A1%2C1%2C0%2C0%2C1%2C28.55%2C27.48Z%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E'); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.clr-i-outline%7Bfill-rule%3Aevenodd%3Bclip-rule%3Aevenodd%3Bfill%3A%23747474%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-triangle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C21.32a1.3%2C1.3%2C0%2C0%2C0%2C1.3-1.3V14a1.3%2C1.3%2C0%2C1%2C0-2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C0%2C18%2C21.32Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20cx%3D%2217.95%22%20cy%3D%2224.27%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20d%3D%22M30.33%2C25.54%2C20.59%2C7.6a3%2C3%2C0%2C0%2C0-5.27%2C0L5.57%2C25.54A3%2C3%2C0%2C0%2C0%2C8.21%2C30H27.69a3%2C3%2C0%2C0%2C0%2C2.64-4.43Zm-1.78%2C1.94a1%2C1%2C0%2C0%2C1-.86.49H8.21a1%2C1%2C0%2C0%2C1-.88-1.48L17.07%2C8.55a1%2C1%2C0%2C0%2C1%2C1.76%2C0l9.74%2C17.94A1%2C1%2C0%2C0%2C1%2C28.55%2C27.48Z%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E"); } .alert-icon.clr-icon-warning-white, .clr-icon.clr-icon-warning-white { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.clr-i-outline%7Bfill-rule%3Aevenodd%3Bclip-rule%3Aevenodd%3Bfill%3A%23ffffff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-triangle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C21.32a1.3%2C1.3%2C0%2C0%2C0%2C1.3-1.3V14a1.3%2C1.3%2C0%2C1%2C0-2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C0%2C18%2C21.32Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20cx%3D%2217.95%22%20cy%3D%2224.27%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20d%3D%22M30.33%2C25.54%2C20.59%2C7.6a3%2C3%2C0%2C0%2C0-5.27%2C0L5.57%2C25.54A3%2C3%2C0%2C0%2C0%2C8.21%2C30H27.69a3%2C3%2C0%2C0%2C0%2C2.64-4.43Zm-1.78%2C1.94a1%2C1%2C0%2C0%2C1-.86.49H8.21a1%2C1%2C0%2C0%2C1-.88-1.48L17.07%2C8.55a1%2C1%2C0%2C0%2C1%2C1.76%2C0l9.74%2C17.94A1%2C1%2C0%2C0%2C1%2C28.55%2C27.48Z%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E'); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%0A%09.clr-i-outline%7Bfill-rule%3Aevenodd%3Bclip-rule%3Aevenodd%3Bfill%3A%23ffffff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-triangle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C21.32a1.3%2C1.3%2C0%2C0%2C0%2C1.3-1.3V14a1.3%2C1.3%2C0%2C1%2C0-2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C0%2C18%2C21.32Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20cx%3D%2217.95%22%20cy%3D%2224.27%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20d%3D%22M30.33%2C25.54%2C20.59%2C7.6a3%2C3%2C0%2C0%2C0-5.27%2C0L5.57%2C25.54A3%2C3%2C0%2C0%2C0%2C8.21%2C30H27.69a3%2C3%2C0%2C0%2C0%2C2.64-4.43Zm-1.78%2C1.94a1%2C1%2C0%2C0%2C1-.86.49H8.21a1%2C1%2C0%2C0%2C1-.88-1.48L17.07%2C8.55a1%2C1%2C0%2C0%2C1%2C1.76%2C0l9.74%2C17.94A1%2C1%2C0%2C0%2C1%2C28.55%2C27.48Z%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E"); } .alert-icon.clr-vmw-logo, .clr-icon.clr-vmw-logo { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2036%2036%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Ctitle%3Evm%20bug%3C%2Ftitle%3E%0A%20%20%20%20%3Cdefs%3E%3C%2Fdefs%3E%0A%20%20%20%20%3Cg%20id%3D%22Headers%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22CL-Headers-Specs%22%20transform%3D%22translate(-262.000000%2C%20-175.000000)%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%2201%22%20transform%3D%22translate(238.000000%2C%20163.000000)%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22vm-bug%22%20transform%3D%22translate(24.703125%2C%2012.000000)%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20id%3D%22Rectangle-42%22%20fill-opacity%3D%220.25%22%20fill%3D%22%23DDDDDD%22%20opacity%3D%220.6%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2236%22%20height%3D%2236%22%20rx%3D%223%22%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M7.63948376%2C13.8762402%20C7.32265324%2C13.2097082%206.53978152%2C12.9085139%205.80923042%2C13.219934%20C5.07771043%2C13.5322837%204.80932495%2C14.3103691%205.13972007%2C14.9769011%20L8.20725954%2C21.3744923%20C8.68977207%2C22.3784735%209.19844491%2C22.9037044%2010.1528121%2C22.9037044%20C11.1720955%2C22.9037044%2011.6168209%2C22.3310633%2012.0983646%2C21.3744923%20C12.0983646%2C21.3744923%2014.7744682%2C15.7847341%2014.8015974%2C15.7261685%20C14.8287266%2C15.6666733%2014.9149588%2C15.4863286%2015.1872199%2C15.4872582%20C15.4178182%2C15.490047%2015.6106294%2C15.6657437%2015.6106294%2C15.9018652%20L15.6106294%2C21.3698443%20C15.6106294%2C22.212073%2016.0979865%2C22.9037044%2017.0349134%2C22.9037044%20C17.9718403%2C22.9037044%2018.4785754%2C22.212073%2018.4785754%2C21.3698443%20L18.4785754%2C16.8965503%20C18.4785754%2C16.0338702%2019.1219254%2C15.4742436%2020.0007183%2C15.4742436%20C20.8785423%2C15.4742436%2021.4637583%2C16.0524624%2021.4637583%2C16.8965503%20L21.4637583%2C21.3698443%20C21.4637583%2C22.212073%2021.9520842%2C22.9037044%2022.8880423%2C22.9037044%20C23.8240003%2C22.9037044%2024.3326731%2C22.212073%2024.3326731%2C21.3698443%20L24.3326731%2C16.8965503%20C24.3326731%2C16.0338702%2024.9750543%2C15.4742436%2025.8538472%2C15.4742436%20C26.7307023%2C15.4742436%2027.3168871%2C16.0524624%2027.3168871%2C16.8965503%20L27.3168871%2C21.3698443%20C27.3168871%2C22.212073%2027.8052131%2C22.9037044%2028.74214%2C22.9037044%20C29.6771291%2C22.9037044%2030.1848331%2C22.212073%2030.1848331%2C21.3698443%20L30.1848331%2C16.2783582%20C30.1848331%2C14.4070488%2028.6181207%2C13.0962956%2026.7307023%2C13.0962956%20C24.8452216%2C13.0962956%2023.6651006%2C14.3475536%2023.6651006%2C14.3475536%20C23.037253%2C13.5666793%2022.1720247%2C13.0972252%2020.7089847%2C13.0972252%20C19.164557%2C13.0972252%2017.8129406%2C14.3475536%2017.8129406%2C14.3475536%20C17.1841241%2C13.5666793%2016.1154267%2C13.0972252%2015.2308204%2C13.0972252%20C13.8617638%2C13.0972252%2012.7746572%2C13.675444%2012.1119292%2C15.1302871%20L10.1528121%2C19.5608189%20L7.63948376%2C13.8762402%22%20id%3D%22Fill-4%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E'); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2036%2036%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Ctitle%3Evm%20bug%3C%2Ftitle%3E%0A%20%20%20%20%3Cdefs%3E%3C%2Fdefs%3E%0A%20%20%20%20%3Cg%20id%3D%22Headers%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22CL-Headers-Specs%22%20transform%3D%22translate(-262.000000%2C%20-175.000000)%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%2201%22%20transform%3D%22translate(238.000000%2C%20163.000000)%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20id%3D%22vm-bug%22%20transform%3D%22translate(24.703125%2C%2012.000000)%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20id%3D%22Rectangle-42%22%20fill-opacity%3D%220.25%22%20fill%3D%22%23DDDDDD%22%20opacity%3D%220.6%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2236%22%20height%3D%2236%22%20rx%3D%223%22%3E%3C%2Frect%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M7.63948376%2C13.8762402%20C7.32265324%2C13.2097082%206.53978152%2C12.9085139%205.80923042%2C13.219934%20C5.07771043%2C13.5322837%204.80932495%2C14.3103691%205.13972007%2C14.9769011%20L8.20725954%2C21.3744923%20C8.68977207%2C22.3784735%209.19844491%2C22.9037044%2010.1528121%2C22.9037044%20C11.1720955%2C22.9037044%2011.6168209%2C22.3310633%2012.0983646%2C21.3744923%20C12.0983646%2C21.3744923%2014.7744682%2C15.7847341%2014.8015974%2C15.7261685%20C14.8287266%2C15.6666733%2014.9149588%2C15.4863286%2015.1872199%2C15.4872582%20C15.4178182%2C15.490047%2015.6106294%2C15.6657437%2015.6106294%2C15.9018652%20L15.6106294%2C21.3698443%20C15.6106294%2C22.212073%2016.0979865%2C22.9037044%2017.0349134%2C22.9037044%20C17.9718403%2C22.9037044%2018.4785754%2C22.212073%2018.4785754%2C21.3698443%20L18.4785754%2C16.8965503%20C18.4785754%2C16.0338702%2019.1219254%2C15.4742436%2020.0007183%2C15.4742436%20C20.8785423%2C15.4742436%2021.4637583%2C16.0524624%2021.4637583%2C16.8965503%20L21.4637583%2C21.3698443%20C21.4637583%2C22.212073%2021.9520842%2C22.9037044%2022.8880423%2C22.9037044%20C23.8240003%2C22.9037044%2024.3326731%2C22.212073%2024.3326731%2C21.3698443%20L24.3326731%2C16.8965503%20C24.3326731%2C16.0338702%2024.9750543%2C15.4742436%2025.8538472%2C15.4742436%20C26.7307023%2C15.4742436%2027.3168871%2C16.0524624%2027.3168871%2C16.8965503%20L27.3168871%2C21.3698443%20C27.3168871%2C22.212073%2027.8052131%2C22.9037044%2028.74214%2C22.9037044%20C29.6771291%2C22.9037044%2030.1848331%2C22.212073%2030.1848331%2C21.3698443%20L30.1848331%2C16.2783582%20C30.1848331%2C14.4070488%2028.6181207%2C13.0962956%2026.7307023%2C13.0962956%20C24.8452216%2C13.0962956%2023.6651006%2C14.3475536%2023.6651006%2C14.3475536%20C23.037253%2C13.5666793%2022.1720247%2C13.0972252%2020.7089847%2C13.0972252%20C19.164557%2C13.0972252%2017.8129406%2C14.3475536%2017.8129406%2C14.3475536%20C17.1841241%2C13.5666793%2016.1154267%2C13.0972252%2015.2308204%2C13.0972252%20C13.8617638%2C13.0972252%2012.7746572%2C13.675444%2012.1119292%2C15.1302871%20L10.1528121%2C19.5608189%20L7.63948376%2C13.8762402%22%20id%3D%22Fill-4%22%20fill%3D%22%23FFFFFF%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E"); } .is-off-screen { - position: fixed !important; - border: none !important; - height: 1px !important; - width: 1px !important; - left: 0 !important; - top: -convertBaselineToBase20(1) !important; - overflow: hidden !important; - visibility: hidden !important; - padding: 0 !important; - margin: 0 0 -convertBaselineToBase20(1) 0 !important; + position: fixed !important; + border: none !important; + height: 1px !important; + width: 1px !important; + left: 0 !important; + top: -convertBaselineToBase20(1) !important; + overflow: hidden !important; + visibility: hidden !important; + padding: 0 !important; + margin: 0 0 -convertBaselineToBase20(1) 0 !important; } .clr-popover-content { - position: fixed; - z-index: 5000; + position: fixed; + z-index: 5000; } :root { - --clr-btn-vertical-margin: 0.3rem; - --clr-btn-horizontal-margin: 0.6rem; - --clr-btn-horizontal-padding: 0.6rem; - --clr-btn-vertical-padding: 0; - --clr-btn-padding: var(--clr-btn-vertical-padding) - var(--clr-btn-horizontal-padding); - --clr-btn-height: 1.8rem; - --clr-btn-height-sm: 1.2rem; - --clr-btn-font-weight: 500; - --clr-btn-border-radius: 0.15rem; - --clr-btn-border-width: 0.05rem; - --clr-btn-outline-bg-color: transparent; - --clr-btn-appearance-standard-line-height: 1.15rem; - --clr-btn-appearance-standard-font-size: 0.55rem; - --clr-btn-appearance-standard-font-weight: 500; - --clr-btn-appearance-standard-height: 1.2rem; - --clr-btn-appearance-standard-padding: 0 0.6rem; - --clr-btn-appearance-standard-icon-size: 0.6rem; - --clr-btn-appearance-form-line-height: 1.8rem; - --clr-btn-appearance-form-letter-spacing: 0.12em; - --clr-btn-appearance-form-font-size: 0.6rem; - --clr-btn-appearance-form-font-weight: 500; - --clr-btn-appearance-form-height: 1.8rem; - --clr-btn-appearance-form-padding: 0 0.6rem; - --clr-btn-default-color: var(--clr-color-action-600); - --clr-btn-default-border-color: var(--clr-btn-default-color); - --clr-btn-default-bg-color: transparent; - --clr-btn-default-hover-bg-color: var(--clr-color-action-50); - --clr-btn-default-hover-color: var(--clr-color-action-800); - --clr-btn-default-box-shadow-color: var(--clr-color-action-500); - --clr-btn-default-disabled-color: var(--clr-color-neutral-700); - --clr-btn-default-disabled-bg-color: var(--clr-btn-default-bg-color); - --clr-btn-default-disabled-border-color: var(--clr-color-neutral-600); - --clr-btn-default-checked-bg-color: var(--clr-color-action-600); - --clr-btn-default-checked-color: var(--clr-color-neutral-0); - --clr-btn-default-outline-color: var(--clr-color-action-600); - --clr-btn-default-outline-border-color: var( - --clr-btn-default-outline-color - ); - --clr-btn-default-outline-bg-color: var(--clr-btn-outline-bg-color); - --clr-btn-default-outline-hover-bg-color: var(--clr-color-action-50); - --clr-btn-default-outline-hover-color: var(--clr-color-action-800); - --clr-btn-default-outline-box-shadow-color: var(--clr-color-action-500); - --clr-btn-default-outline-disabled-color: var(--clr-color-neutral-700); - --clr-btn-default-outline-disabled-bg-color: var( - --clr-btn-default-outline-bg-color - ); - --clr-btn-default-outline-disabled-border-color: var( - --clr-color-neutral-600 - ); - --clr-btn-default-outline-checked-color: var(--clr-color-neutral-0); - --clr-btn-default-outline-checked-bg-color: var( - --clr-btn-default-outline-color - ); - --clr-btn-primary-color: var(--clr-color-neutral-0); - --clr-btn-primary-bg-color: var(--clr-color-action-600); - --clr-btn-primary-border-color: var(--clr-color-action-600); - --clr-btn-primary-hover-bg-color: var(--clr-color-action-800); - --clr-btn-primary-hover-color: var(--clr-color-action-50); - --clr-btn-primary-box-shadow-color: var(--clr-color-action-500); - --clr-btn-primary-disabled-color: var(--clr-color-neutral-700); - --clr-btn-primary-disabled-bg-color: var(--clr-color-neutral-400); - --clr-btn-primary-disabled-border-color: var(--clr-color-neutral-400); - --clr-btn-primary-checked-bg-color: var(--clr-color-action-600); - --clr-btn-primary-checked-color: var(--clr-color-neutral-0); - --clr-btn-success-color: var(--clr-color-neutral-0); - --clr-btn-success-bg-color: var(--clr-color-success-700); - --clr-btn-success-border-color: var(--clr-btn-success-bg-color); - --clr-btn-success-hover-bg-color: var(--clr-color-success-800); - --clr-btn-success-hover-color: var(--clr-btn-success-color); - --clr-btn-success-box-shadow-color: var(--clr-color-success-900); - --clr-btn-success-disabled-color: var(--clr-color-neutral-700); - --clr-btn-success-disabled-bg-color: var(--clr-color-neutral-400); - --clr-btn-success-disabled-border-color: var(--clr-color-neutral-400); - --clr-btn-success-checked-bg-color: var(--clr-btn-success-hover-bg-color); - --clr-btn-success-checked-color: var(--clr-btn-success-color); - --clr-btn-success-outline-color: var(--clr-color-success-700); - --clr-btn-success-outline-bg-color: var(--clr-btn-outline-bg-color); - --clr-btn-success-outline-border-color: var(--clr-color-success-700); - --clr-btn-success-outline-hover-bg-color: var(--clr-color-success-50); - --clr-btn-success-outline-hover-color: var(--clr-color-success-900); - --clr-btn-success-outline-box-shadow-color: var(--clr-color-success-400); - --clr-btn-success-outline-disabled-color: var(--clr-color-neutral-700); - --clr-btn-success-outline-disabled-bg-color: var( - --clr-btn-success-outline-bg-color - ); - --clr-btn-success-outline-disabled-border-color: var( - --clr-color-neutral-600 - ); - --clr-btn-success-outline-checked-bg-color: var( - --clr-btn-success-outline-border-color - ); - --clr-btn-success-outline-checked-color: var(--clr-color-neutral-0); - --clr-btn-danger-color: var(--clr-color-neutral-0); - --clr-btn-danger-bg-color: var(--clr-color-danger-700); - --clr-btn-danger-border-color: var(--clr-btn-danger-bg-color); - --clr-btn-danger-hover-bg-color: var(--clr-color-danger-800); - --clr-btn-danger-hover-color: var(--clr-btn-danger-color); - --clr-btn-danger-box-shadow-color: var(--clr-color-danger-900); - --clr-btn-danger-disabled-color: var(--clr-color-neutral-700); - --clr-btn-danger-disabled-bg-color: var(--clr-color-neutral-400); - --clr-btn-danger-disabled-border-color: var( - --clr-btn-danger-disabled-color - ); - --clr-btn-danger-checked-bg-color: var(--clr-color-danger-800); - --clr-btn-danger-checked-color: var(--clr-btn-danger-color); - --clr-btn-danger-outline-color: var(--clr-color-danger-700); - --clr-btn-danger-outline-bg-color: var(--clr-btn-outline-bg-color); - --clr-btn-danger-outline-border-color: var(--clr-color-danger-800); - --clr-btn-danger-outline-hover-bg-color: var(--clr-color-danger-100); - --clr-btn-danger-outline-hover-color: var(--clr-color-danger-900); - --clr-btn-danger-outline-box-shadow-color: var(--clr-color-danger-200); - --clr-btn-danger-outline-disabled-color: var(--clr-color-neutral-700); - --clr-btn-danger-outline-disabled-bg-color: var( - --clr-btn-danger-outline-bg-color - ); - --clr-btn-danger-outline-disabled-border-color: var( - --clr-btn-danger-outline-disabled-color - ); - --clr-btn-danger-outline-checked-bg-color: var(--clr-color-danger-800); - --clr-btn-danger-outline-checked-color: var(--clr-color-neutral-0); - --clr-btn-link-color: var(--clr-color-action-600); - --clr-btn-link-bg-color: transparent; - --clr-btn-link-border-color: transparent; - --clr-btn-link-hover-bg-color: transparent; - --clr-btn-link-hover-color: var(--clr-color-action-800); - --clr-btn-link-disabled-color: var(--clr-color-neutral-700); - --clr-btn-link-disabled-bg-color: transparent; - --clr-btn-link-disabled-border-color: transparent; - --clr-btn-link-checked-bg-color: transparent; - --clr-btn-link-checked-color: var(--clr-color-action-800); - --clr-btn-inverse-color: var(--clr-color-neutral-0); - --clr-btn-inverse-border-color: var(--clr-color-neutral-0); - --clr-btn-inverse-bg-color: transparent; - --clr-btn-inverse-hover-bg-color: rgba(255, 255, 255, 0.15); - --clr-btn-inverse-hover-color: var(--clr-color-neutral-0); - --clr-btn-inverse-box-shadow-color: rgba(0, 0, 0, 0.25); - --clr-btn-inverse-disabled-color: var(--clr-color-neutral-0); - --clr-btn-inverse-disabled-bg-color: var(--clr-btn-inverse-bg-color); - --clr-btn-inverse-disabled-border-color: var(--clr-color-neutral-0); - --clr-btn-inverse-checked-bg-color: rgba(255, 255, 255, 0.15); - --clr-btn-inverse-checked-color: var(--clr-color-neutral-0); - --clr-btn-icon-disabled-color: var( - --clr-btn-default-disabled-color, - #666666 - ); - --clr-btn-group-focus-outline: #51cbee; + --clr-btn-vertical-margin: 0.3rem; + --clr-btn-horizontal-margin: 0.6rem; + --clr-btn-horizontal-padding: 0.6rem; + --clr-btn-vertical-padding: 0; + --clr-btn-padding: var(--clr-btn-vertical-padding) + var(--clr-btn-horizontal-padding); + --clr-btn-height: 1.8rem; + --clr-btn-height-sm: 1.2rem; + --clr-btn-font-weight: 500; + --clr-btn-border-radius: 0.15rem; + --clr-btn-border-width: 0.05rem; + --clr-btn-outline-bg-color: transparent; + --clr-btn-appearance-standard-line-height: 1.15rem; + --clr-btn-appearance-standard-font-size: 0.55rem; + --clr-btn-appearance-standard-font-weight: 500; + --clr-btn-appearance-standard-height: 1.2rem; + --clr-btn-appearance-standard-padding: 0 0.6rem; + --clr-btn-appearance-standard-icon-size: 0.6rem; + --clr-btn-appearance-form-line-height: 1.8rem; + --clr-btn-appearance-form-letter-spacing: 0.12em; + --clr-btn-appearance-form-font-size: 0.6rem; + --clr-btn-appearance-form-font-weight: 500; + --clr-btn-appearance-form-height: 1.8rem; + --clr-btn-appearance-form-padding: 0 0.6rem; + --clr-btn-default-color: var(--clr-color-action-600); + --clr-btn-default-border-color: var(--clr-btn-default-color); + --clr-btn-default-bg-color: transparent; + --clr-btn-default-hover-bg-color: var(--clr-color-action-50); + --clr-btn-default-hover-color: var(--clr-color-action-800); + --clr-btn-default-box-shadow-color: var(--clr-color-action-500); + --clr-btn-default-disabled-color: var(--clr-color-neutral-700); + --clr-btn-default-disabled-bg-color: var(--clr-btn-default-bg-color); + --clr-btn-default-disabled-border-color: var(--clr-color-neutral-600); + --clr-btn-default-checked-bg-color: var(--clr-color-action-600); + --clr-btn-default-checked-color: var(--clr-color-neutral-0); + --clr-btn-default-outline-color: var(--clr-color-action-600); + --clr-btn-default-outline-border-color: var(--clr-btn-default-outline-color); + --clr-btn-default-outline-bg-color: var(--clr-btn-outline-bg-color); + --clr-btn-default-outline-hover-bg-color: var(--clr-color-action-50); + --clr-btn-default-outline-hover-color: var(--clr-color-action-800); + --clr-btn-default-outline-box-shadow-color: var(--clr-color-action-500); + --clr-btn-default-outline-disabled-color: var(--clr-color-neutral-700); + --clr-btn-default-outline-disabled-bg-color: var( + --clr-btn-default-outline-bg-color + ); + --clr-btn-default-outline-disabled-border-color: var(--clr-color-neutral-600); + --clr-btn-default-outline-checked-color: var(--clr-color-neutral-0); + --clr-btn-default-outline-checked-bg-color: var( + --clr-btn-default-outline-color + ); + --clr-btn-primary-color: var(--clr-color-neutral-0); + --clr-btn-primary-bg-color: var(--clr-color-action-600); + --clr-btn-primary-border-color: var(--clr-color-action-600); + --clr-btn-primary-hover-bg-color: var(--clr-color-action-800); + --clr-btn-primary-hover-color: var(--clr-color-action-50); + --clr-btn-primary-box-shadow-color: var(--clr-color-action-500); + --clr-btn-primary-disabled-color: var(--clr-color-neutral-700); + --clr-btn-primary-disabled-bg-color: var(--clr-color-neutral-400); + --clr-btn-primary-disabled-border-color: var(--clr-color-neutral-400); + --clr-btn-primary-checked-bg-color: var(--clr-color-action-600); + --clr-btn-primary-checked-color: var(--clr-color-neutral-0); + --clr-btn-success-color: var(--clr-color-neutral-0); + --clr-btn-success-bg-color: var(--clr-color-success-700); + --clr-btn-success-border-color: var(--clr-btn-success-bg-color); + --clr-btn-success-hover-bg-color: var(--clr-color-success-800); + --clr-btn-success-hover-color: var(--clr-btn-success-color); + --clr-btn-success-box-shadow-color: var(--clr-color-success-900); + --clr-btn-success-disabled-color: var(--clr-color-neutral-700); + --clr-btn-success-disabled-bg-color: var(--clr-color-neutral-400); + --clr-btn-success-disabled-border-color: var(--clr-color-neutral-400); + --clr-btn-success-checked-bg-color: var(--clr-btn-success-hover-bg-color); + --clr-btn-success-checked-color: var(--clr-btn-success-color); + --clr-btn-success-outline-color: var(--clr-color-success-700); + --clr-btn-success-outline-bg-color: var(--clr-btn-outline-bg-color); + --clr-btn-success-outline-border-color: var(--clr-color-success-700); + --clr-btn-success-outline-hover-bg-color: var(--clr-color-success-50); + --clr-btn-success-outline-hover-color: var(--clr-color-success-900); + --clr-btn-success-outline-box-shadow-color: var(--clr-color-success-400); + --clr-btn-success-outline-disabled-color: var(--clr-color-neutral-700); + --clr-btn-success-outline-disabled-bg-color: var( + --clr-btn-success-outline-bg-color + ); + --clr-btn-success-outline-disabled-border-color: var(--clr-color-neutral-600); + --clr-btn-success-outline-checked-bg-color: var( + --clr-btn-success-outline-border-color + ); + --clr-btn-success-outline-checked-color: var(--clr-color-neutral-0); + --clr-btn-danger-color: var(--clr-color-neutral-0); + --clr-btn-danger-bg-color: var(--clr-color-danger-700); + --clr-btn-danger-border-color: var(--clr-btn-danger-bg-color); + --clr-btn-danger-hover-bg-color: var(--clr-color-danger-800); + --clr-btn-danger-hover-color: var(--clr-btn-danger-color); + --clr-btn-danger-box-shadow-color: var(--clr-color-danger-900); + --clr-btn-danger-disabled-color: var(--clr-color-neutral-700); + --clr-btn-danger-disabled-bg-color: var(--clr-color-neutral-400); + --clr-btn-danger-disabled-border-color: var(--clr-btn-danger-disabled-color); + --clr-btn-danger-checked-bg-color: var(--clr-color-danger-800); + --clr-btn-danger-checked-color: var(--clr-btn-danger-color); + --clr-btn-danger-outline-color: var(--clr-color-danger-700); + --clr-btn-danger-outline-bg-color: var(--clr-btn-outline-bg-color); + --clr-btn-danger-outline-border-color: var(--clr-color-danger-800); + --clr-btn-danger-outline-hover-bg-color: var(--clr-color-danger-100); + --clr-btn-danger-outline-hover-color: var(--clr-color-danger-900); + --clr-btn-danger-outline-box-shadow-color: var(--clr-color-danger-200); + --clr-btn-danger-outline-disabled-color: var(--clr-color-neutral-700); + --clr-btn-danger-outline-disabled-bg-color: var( + --clr-btn-danger-outline-bg-color + ); + --clr-btn-danger-outline-disabled-border-color: var( + --clr-btn-danger-outline-disabled-color + ); + --clr-btn-danger-outline-checked-bg-color: var(--clr-color-danger-800); + --clr-btn-danger-outline-checked-color: var(--clr-color-neutral-0); + --clr-btn-link-color: var(--clr-color-action-600); + --clr-btn-link-bg-color: transparent; + --clr-btn-link-border-color: transparent; + --clr-btn-link-hover-bg-color: transparent; + --clr-btn-link-hover-color: var(--clr-color-action-800); + --clr-btn-link-disabled-color: var(--clr-color-neutral-700); + --clr-btn-link-disabled-bg-color: transparent; + --clr-btn-link-disabled-border-color: transparent; + --clr-btn-link-checked-bg-color: transparent; + --clr-btn-link-checked-color: var(--clr-color-action-800); + --clr-btn-inverse-color: var(--clr-color-neutral-0); + --clr-btn-inverse-border-color: var(--clr-color-neutral-0); + --clr-btn-inverse-bg-color: transparent; + --clr-btn-inverse-hover-bg-color: rgba(255, 255, 255, 0.15); + --clr-btn-inverse-hover-color: var(--clr-color-neutral-0); + --clr-btn-inverse-box-shadow-color: rgba(0, 0, 0, 0.25); + --clr-btn-inverse-disabled-color: var(--clr-color-neutral-0); + --clr-btn-inverse-disabled-bg-color: var(--clr-btn-inverse-bg-color); + --clr-btn-inverse-disabled-border-color: var(--clr-color-neutral-0); + --clr-btn-inverse-checked-bg-color: rgba(255, 255, 255, 0.15); + --clr-btn-inverse-checked-color: var(--clr-color-neutral-0); + --clr-btn-icon-disabled-color: var(--clr-btn-default-disabled-color, #666666); + --clr-btn-group-focus-outline: #51cbee; } .btn { - cursor: pointer; - display: inline-block; - -webkit-appearance: none !important; - border-radius: 0.15rem; - border-radius: var(--clr-btn-border-radius, 0.15rem); - border-width: 0.05rem; - border-width: var(--clr-btn-border-width, 0.05rem); - border-style: solid; - min-width: 3.6rem; - max-width: 18rem; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - text-align: center; - text-decoration: none; - text-transform: uppercase; - vertical-align: middle; - border-color: #0072a3; - border-color: var(--clr-btn-default-border-color, #0072a3); - background-color: transparent; - background-color: var(--clr-btn-default-bg-color, transparent); - color: #0072a3; - color: var(--clr-btn-default-color, #0072a3); - line-height: 1.8rem; - line-height: var(--clr-btn-appearance-form-line-height, 1.8rem); - letter-spacing: 0.12em; - font-size: 0.6rem; - font-size: var(--clr-btn-appearance-form-font-size, 0.6rem); - font-weight: 500; - font-weight: var(--clr-btn-appearance-form-font-weight, 500); - height: 1.8rem; - height: var(--clr-btn-appearance-form-height, 1.8rem); - padding: 0 0.6rem; - padding: var(--clr-btn-appearance-form-padding, 0 0.6rem); + cursor: pointer; + display: inline-block; + -webkit-appearance: none !important; + border-radius: 0.15rem; + border-radius: var(--clr-btn-border-radius, 0.15rem); + border-width: 0.05rem; + border-width: var(--clr-btn-border-width, 0.05rem); + border-style: solid; + min-width: 3.6rem; + max-width: 18rem; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + text-align: center; + text-decoration: none; + text-transform: uppercase; + vertical-align: middle; + border-color: #0072a3; + border-color: var(--clr-btn-default-border-color, #0072a3); + background-color: transparent; + background-color: var(--clr-btn-default-bg-color, transparent); + color: #0072a3; + color: var(--clr-btn-default-color, #0072a3); + line-height: 1.8rem; + line-height: var(--clr-btn-appearance-form-line-height, 1.8rem); + letter-spacing: 0.12em; + font-size: 0.6rem; + font-size: var(--clr-btn-appearance-form-font-size, 0.6rem); + font-weight: 500; + font-weight: var(--clr-btn-appearance-form-font-weight, 500); + height: 1.8rem; + height: var(--clr-btn-appearance-form-height, 1.8rem); + padding: 0 0.6rem; + padding: var(--clr-btn-appearance-form-padding, 0 0.6rem); } .btn:hover { - text-decoration: none; + text-decoration: none; } .btn cds-icon { - color: #0072a3; - color: var(--clr-btn-default-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-default-color, #0072a3); } .btn:visited { - color: #0072a3; - color: var(--clr-btn-default-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-default-color, #0072a3); } .btn:hover { - background-color: #e3f5fc; - background-color: var(--clr-btn-default-hover-bg-color, #e3f5fc); - color: #00567a; - color: var(--clr-btn-default-hover-color, #00567a); + background-color: #e3f5fc; + background-color: var(--clr-btn-default-hover-bg-color, #e3f5fc); + color: #00567a; + color: var(--clr-btn-default-hover-color, #00567a); } .btn:active { - -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; - box-shadow: 0 0.1rem 0 0 #179bd3 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-default-box-shadow-color, #179bd3) inset; - box-shadow: 0 0.1rem 0 0 var(--clr-btn-default-box-shadow-color, #179bd3) - inset; + -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; + box-shadow: 0 0.1rem 0 0 #179bd3 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-default-box-shadow-color, #179bd3) inset; + box-shadow: 0 0.1rem 0 0 var(--clr-btn-default-box-shadow-color, #179bd3) + inset; } .btn.disabled, .btn:disabled { - color: #666; - color: var(--clr-btn-default-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var(--clr-btn-default-disabled-bg-color, transparent); - border-color: #8c8c8c; - border-color: var(--clr-btn-default-disabled-border-color, #8c8c8c); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-default-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var(--clr-btn-default-disabled-bg-color, transparent); + border-color: #8c8c8c; + border-color: var(--clr-btn-default-disabled-border-color, #8c8c8c); + -webkit-box-shadow: none; + box-shadow: none; } .btn cds-icon, .btn-group > .btn cds-icon { - -webkit-transform: translate3d(0, -0.1rem, 0); - transform: translate3d(0, -0.1rem, 0); + -webkit-transform: translate3d(0, -0.1rem, 0); + transform: translate3d(0, -0.1rem, 0); } .btn-info .btn, .btn-info-outline .btn, @@ -3591,12 +3580,12 @@ a:visited { .btn.btn-primary-outline, .btn.btn-secondary, .btn.btn-secondary-outline { - border-color: #0072a3; - border-color: var(--clr-btn-default-border-color, #0072a3); - background-color: transparent; - background-color: var(--clr-btn-default-bg-color, transparent); - color: #0072a3; - color: var(--clr-btn-default-color, #0072a3); + border-color: #0072a3; + border-color: var(--clr-btn-default-border-color, #0072a3); + background-color: transparent; + background-color: var(--clr-btn-default-bg-color, transparent); + color: #0072a3; + color: var(--clr-btn-default-color, #0072a3); } .btn-info .btn cds-icon, .btn-info-outline .btn cds-icon, @@ -3615,8 +3604,8 @@ a:visited { .btn.btn-primary-outline cds-icon, .btn.btn-secondary cds-icon, .btn.btn-secondary-outline cds-icon { - color: #0072a3; - color: var(--clr-btn-default-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-default-color, #0072a3); } .btn-info .btn:visited, .btn-info-outline .btn:visited, @@ -3635,8 +3624,8 @@ a:visited { .btn.btn-primary-outline:visited, .btn.btn-secondary-outline:visited, .btn.btn-secondary:visited { - color: #0072a3; - color: var(--clr-btn-default-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-default-color, #0072a3); } .btn-info .btn:hover, .btn-info-outline .btn:hover, @@ -3655,10 +3644,10 @@ a:visited { .btn.btn-primary-outline:hover, .btn.btn-secondary-outline:hover, .btn.btn-secondary:hover { - background-color: #e3f5fc; - background-color: var(--clr-btn-default-hover-bg-color, #e3f5fc); - color: #00567a; - color: var(--clr-btn-default-hover-color, #00567a); + background-color: #e3f5fc; + background-color: var(--clr-btn-default-hover-bg-color, #e3f5fc); + color: #00567a; + color: var(--clr-btn-default-hover-color, #00567a); } .btn-info .btn:active, .btn-info-outline .btn:active, @@ -3677,12 +3666,12 @@ a:visited { .btn.btn-primary-outline:active, .btn.btn-secondary-outline:active, .btn.btn-secondary:active { - -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; - box-shadow: 0 0.1rem 0 0 #179bd3 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-default-box-shadow-color, #179bd3) inset; - box-shadow: 0 0.1rem 0 0 var(--clr-btn-default-box-shadow-color, #179bd3) - inset; + -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; + box-shadow: 0 0.1rem 0 0 #179bd3 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-default-box-shadow-color, #179bd3) inset; + box-shadow: 0 0.1rem 0 0 var(--clr-btn-default-box-shadow-color, #179bd3) + inset; } .btn-info .btn.disabled, .btn-info .btn:disabled, @@ -3718,161 +3707,160 @@ a:visited { .btn.btn-secondary-outline:disabled, .btn.btn-secondary.disabled, .btn.btn-secondary:disabled { - color: #666; - color: var(--clr-btn-default-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var(--clr-btn-default-disabled-bg-color, transparent); - border-color: #8c8c8c; - border-color: var(--clr-btn-default-disabled-border-color, #8c8c8c); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-default-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var(--clr-btn-default-disabled-bg-color, transparent); + border-color: #8c8c8c; + border-color: var(--clr-btn-default-disabled-border-color, #8c8c8c); + -webkit-box-shadow: none; + box-shadow: none; } .btn-primary .btn, .btn.btn-primary { - border-color: #0072a3; - border-color: var(--clr-btn-primary-border-color, #0072a3); - background-color: #0072a3; - background-color: var(--clr-btn-primary-bg-color, #0072a3); - color: #fff; - color: var(--clr-btn-primary-color, #fff); + border-color: #0072a3; + border-color: var(--clr-btn-primary-border-color, #0072a3); + background-color: #0072a3; + background-color: var(--clr-btn-primary-bg-color, #0072a3); + color: #fff; + color: var(--clr-btn-primary-color, #fff); } .btn-primary .btn cds-icon, .btn.btn-primary cds-icon { - color: #fff; - color: var(--clr-btn-primary-color, #fff); + color: #fff; + color: var(--clr-btn-primary-color, #fff); } .btn-primary .btn:visited, .btn.btn-primary:visited { - color: #fff; - color: var(--clr-btn-primary-color, #fff); + color: #fff; + color: var(--clr-btn-primary-color, #fff); } .btn-primary .btn:hover, .btn.btn-primary:hover { - background-color: #00567a; - background-color: var(--clr-btn-primary-hover-bg-color, #00567a); - color: #e3f5fc; - color: var(--clr-btn-primary-hover-color, #e3f5fc); + background-color: #00567a; + background-color: var(--clr-btn-primary-hover-bg-color, #00567a); + color: #e3f5fc; + color: var(--clr-btn-primary-hover-color, #e3f5fc); } .btn-primary .btn:active, .btn.btn-primary:active { - -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; - box-shadow: 0 0.1rem 0 0 #179bd3 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-primary-box-shadow-color, #179bd3) inset; - box-shadow: 0 0.1rem 0 0 var(--clr-btn-primary-box-shadow-color, #179bd3) - inset; + -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; + box-shadow: 0 0.1rem 0 0 #179bd3 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-primary-box-shadow-color, #179bd3) inset; + box-shadow: 0 0.1rem 0 0 var(--clr-btn-primary-box-shadow-color, #179bd3) + inset; } .btn-primary .btn.disabled, .btn-primary .btn:disabled, .btn.btn-primary.disabled, .btn.btn-primary:disabled { - color: #666; - color: var(--clr-btn-primary-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: #ccc; - background-color: var(--clr-btn-primary-disabled-bg-color, #ccc); - border-color: #ccc; - border-color: var(--clr-btn-primary-disabled-border-color, #ccc); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-primary-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: #ccc; + background-color: var(--clr-btn-primary-disabled-bg-color, #ccc); + border-color: #ccc; + border-color: var(--clr-btn-primary-disabled-border-color, #ccc); + -webkit-box-shadow: none; + box-shadow: none; } .btn-success .btn, .btn.btn-success { - border-color: #3c8500; - border-color: var(--clr-btn-success-border-color, #3c8500); - background-color: #3c8500; - background-color: var(--clr-btn-success-bg-color, #3c8500); - color: #fff; - color: var(--clr-btn-success-color, #fff); + border-color: #3c8500; + border-color: var(--clr-btn-success-border-color, #3c8500); + background-color: #3c8500; + background-color: var(--clr-btn-success-bg-color, #3c8500); + color: #fff; + color: var(--clr-btn-success-color, #fff); } .btn-success .btn cds-icon, .btn.btn-success cds-icon { - color: #fff; - color: var(--clr-btn-success-color, #fff); + color: #fff; + color: var(--clr-btn-success-color, #fff); } .btn-success .btn:visited, .btn.btn-success:visited { - color: #fff; - color: var(--clr-btn-success-color, #fff); + color: #fff; + color: var(--clr-btn-success-color, #fff); } .btn-success .btn:hover, .btn.btn-success:hover { - background-color: #306b00; - background-color: var(--clr-btn-success-hover-bg-color, #306b00); - color: #fff; - color: var(--clr-btn-success-hover-color, #fff); + background-color: #306b00; + background-color: var(--clr-btn-success-hover-bg-color, #306b00); + color: #fff; + color: var(--clr-btn-success-hover-color, #fff); } .btn-success .btn:active, .btn.btn-success:active { - -webkit-box-shadow: 0 0.1rem 0 0 #255200 inset; - box-shadow: 0 0.1rem 0 0 #255200 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-success-box-shadow-color, #255200) inset; - box-shadow: 0 0.1rem 0 0 var(--clr-btn-success-box-shadow-color, #255200) - inset; + -webkit-box-shadow: 0 0.1rem 0 0 #255200 inset; + box-shadow: 0 0.1rem 0 0 #255200 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-success-box-shadow-color, #255200) inset; + box-shadow: 0 0.1rem 0 0 var(--clr-btn-success-box-shadow-color, #255200) + inset; } .btn-success .btn.disabled, .btn-success .btn:disabled, .btn.btn-success.disabled, .btn.btn-success:disabled { - color: #666; - color: var(--clr-btn-success-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: #ccc; - background-color: var(--clr-btn-success-disabled-bg-color, #ccc); - border-color: #ccc; - border-color: var(--clr-btn-success-disabled-border-color, #ccc); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-success-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: #ccc; + background-color: var(--clr-btn-success-disabled-bg-color, #ccc); + border-color: #ccc; + border-color: var(--clr-btn-success-disabled-border-color, #ccc); + -webkit-box-shadow: none; + box-shadow: none; } .btn-danger .btn, .btn-warning .btn, .btn.btn-danger, .btn.btn-warning { - border-color: #db2100; - border-color: var(--clr-btn-danger-border-color, #db2100); - background-color: #db2100; - background-color: var(--clr-btn-danger-bg-color, #db2100); - color: #fff; - color: var(--clr-btn-danger-color, #fff); + border-color: #db2100; + border-color: var(--clr-btn-danger-border-color, #db2100); + background-color: #db2100; + background-color: var(--clr-btn-danger-bg-color, #db2100); + color: #fff; + color: var(--clr-btn-danger-color, #fff); } .btn-danger .btn cds-icon, .btn-warning .btn cds-icon, .btn.btn-danger cds-icon, .btn.btn-warning cds-icon { - color: #fff; - color: var(--clr-btn-danger-color, #fff); + color: #fff; + color: var(--clr-btn-danger-color, #fff); } .btn-danger .btn:visited, .btn-warning .btn:visited, .btn.btn-danger:visited, .btn.btn-warning:visited { - color: #fff; - color: var(--clr-btn-danger-color, #fff); + color: #fff; + color: var(--clr-btn-danger-color, #fff); } .btn-danger .btn:hover, .btn-warning .btn:hover, .btn.btn-danger:hover, .btn.btn-warning:hover { - background-color: #c21d00; - background-color: var(--clr-btn-danger-hover-bg-color, #c21d00); - color: #fff; - color: var(--clr-btn-danger-hover-color, #fff); + background-color: #c21d00; + background-color: var(--clr-btn-danger-hover-bg-color, #c21d00); + color: #fff; + color: var(--clr-btn-danger-hover-color, #fff); } .btn-danger .btn:active, .btn-warning .btn:active, .btn.btn-danger:active, .btn.btn-warning:active { - -webkit-box-shadow: 0 0.1rem 0 0 #991700 inset; - box-shadow: 0 0.1rem 0 0 #991700 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-danger-box-shadow-color, #991700) inset; - box-shadow: 0 0.1rem 0 0 var(--clr-btn-danger-box-shadow-color, #991700) - inset; + -webkit-box-shadow: 0 0.1rem 0 0 #991700 inset; + box-shadow: 0 0.1rem 0 0 #991700 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-danger-box-shadow-color, #991700) inset; + box-shadow: 0 0.1rem 0 0 var(--clr-btn-danger-box-shadow-color, #991700) inset; } .btn-danger .btn.disabled, .btn-danger .btn:disabled, @@ -3882,16 +3870,16 @@ a:visited { .btn.btn-danger:disabled, .btn.btn-warning.disabled, .btn.btn-warning:disabled { - color: #666; - color: var(--clr-btn-danger-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: #ccc; - background-color: var(--clr-btn-danger-disabled-bg-color, #ccc); - border-color: #666; - border-color: var(--clr-btn-danger-disabled-border-color, #666); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-danger-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: #ccc; + background-color: var(--clr-btn-danger-disabled-bg-color, #ccc); + border-color: #666; + border-color: var(--clr-btn-danger-disabled-border-color, #666); + -webkit-box-shadow: none; + box-shadow: none; } .btn-info-outline .btn, .btn-outline .btn, @@ -3899,12 +3887,12 @@ a:visited { .btn.btn-outline, .btn.btn-outline .btn, .btn.btn-outline-info { - border-color: #0072a3; - border-color: var(--clr-btn-default-outline-border-color, #0072a3); - background-color: transparent; - background-color: var(--clr-btn-default-outline-bg-color, transparent); - color: #0072a3; - color: var(--clr-btn-default-outline-color, #0072a3); + border-color: #0072a3; + border-color: var(--clr-btn-default-outline-border-color, #0072a3); + background-color: transparent; + background-color: var(--clr-btn-default-outline-bg-color, transparent); + color: #0072a3; + color: var(--clr-btn-default-outline-color, #0072a3); } .btn-info-outline .btn cds-icon, .btn-outline .btn cds-icon, @@ -3912,8 +3900,8 @@ a:visited { .btn.btn-outline .btn cds-icon, .btn.btn-outline cds-icon, .btn.btn-outline-info cds-icon { - color: #0072a3; - color: var(--clr-btn-default-outline-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-default-outline-color, #0072a3); } .btn-info-outline .btn:visited, .btn-outline .btn:visited, @@ -3921,8 +3909,8 @@ a:visited { .btn.btn-outline .btn:visited, .btn.btn-outline-info:visited, .btn.btn-outline:visited { - color: #0072a3; - color: var(--clr-btn-default-outline-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-default-outline-color, #0072a3); } .btn-info-outline .btn:hover, .btn-outline .btn:hover, @@ -3930,10 +3918,10 @@ a:visited { .btn.btn-outline .btn:hover, .btn.btn-outline-info:hover, .btn.btn-outline:hover { - background-color: #e3f5fc; - background-color: var(--clr-btn-default-outline-hover-bg-color, #e3f5fc); - color: #00567a; - color: var(--clr-btn-default-outline-hover-color, #00567a); + background-color: #e3f5fc; + background-color: var(--clr-btn-default-outline-hover-bg-color, #e3f5fc); + color: #00567a; + color: var(--clr-btn-default-outline-hover-color, #00567a); } .btn-info-outline .btn:active, .btn-outline .btn:active, @@ -3941,12 +3929,12 @@ a:visited { .btn.btn-outline .btn:active, .btn.btn-outline-info:active, .btn.btn-outline:active { - -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; - box-shadow: 0 0.1rem 0 0 #179bd3 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-default-outline-box-shadow-color, #179bd3) inset; - box-shadow: 0 0.1rem 0 0 - var(--clr-btn-default-outline-box-shadow-color, #179bd3) inset; + -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; + box-shadow: 0 0.1rem 0 0 #179bd3 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-default-outline-box-shadow-color, #179bd3) inset; + box-shadow: 0 0.1rem 0 0 + var(--clr-btn-default-outline-box-shadow-color, #179bd3) inset; } .btn-info-outline .btn.disabled, .btn-info-outline .btn:disabled, @@ -3960,64 +3948,64 @@ a:visited { .btn.btn-outline-info:disabled, .btn.btn-outline.disabled, .btn.btn-outline:disabled { - color: #666; - color: var(--clr-btn-default-outline-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var( - --clr-btn-default-outline-disabled-bg-color, - transparent - ); - border-color: #8c8c8c; - border-color: var(--clr-btn-default-outline-disabled-border-color, #8c8c8c); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-default-outline-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var( + --clr-btn-default-outline-disabled-bg-color, + transparent + ); + border-color: #8c8c8c; + border-color: var(--clr-btn-default-outline-disabled-border-color, #8c8c8c); + -webkit-box-shadow: none; + box-shadow: none; } .btn-outline-success .btn, .btn-success-outline .btn, .btn.btn-outline-success, .btn.btn-success-outline { - border-color: #3c8500; - border-color: var(--clr-btn-success-outline-border-color, #3c8500); - background-color: transparent; - background-color: var(--clr-btn-success-outline-bg-color, transparent); - color: #3c8500; - color: var(--clr-btn-success-outline-color, #3c8500); + border-color: #3c8500; + border-color: var(--clr-btn-success-outline-border-color, #3c8500); + background-color: transparent; + background-color: var(--clr-btn-success-outline-bg-color, transparent); + color: #3c8500; + color: var(--clr-btn-success-outline-color, #3c8500); } .btn-outline-success .btn cds-icon, .btn-success-outline .btn cds-icon, .btn.btn-outline-success cds-icon, .btn.btn-success-outline cds-icon { - color: #3c8500; - color: var(--clr-btn-success-outline-color, #3c8500); + color: #3c8500; + color: var(--clr-btn-success-outline-color, #3c8500); } .btn-outline-success .btn:visited, .btn-success-outline .btn:visited, .btn.btn-outline-success:visited, .btn.btn-success-outline:visited { - color: #3c8500; - color: var(--clr-btn-success-outline-color, #3c8500); + color: #3c8500; + color: var(--clr-btn-success-outline-color, #3c8500); } .btn-outline-success .btn:hover, .btn-success-outline .btn:hover, .btn.btn-outline-success:hover, .btn.btn-success-outline:hover { - background-color: #dff0d0; - background-color: var(--clr-btn-success-outline-hover-bg-color, #dff0d0); - color: #255200; - color: var(--clr-btn-success-outline-hover-color, #255200); + background-color: #dff0d0; + background-color: var(--clr-btn-success-outline-hover-bg-color, #dff0d0); + color: #255200; + color: var(--clr-btn-success-outline-hover-color, #255200); } .btn-outline-success .btn:active, .btn-success-outline .btn:active, .btn.btn-outline-success:active, .btn.btn-success-outline:active { - -webkit-box-shadow: 0 0.1rem 0 0 #5eb715 inset; - box-shadow: 0 0.1rem 0 0 #5eb715 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-success-outline-box-shadow-color, #5eb715) inset; - box-shadow: 0 0.1rem 0 0 - var(--clr-btn-success-outline-box-shadow-color, #5eb715) inset; + -webkit-box-shadow: 0 0.1rem 0 0 #5eb715 inset; + box-shadow: 0 0.1rem 0 0 #5eb715 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-success-outline-box-shadow-color, #5eb715) inset; + box-shadow: 0 0.1rem 0 0 + var(--clr-btn-success-outline-box-shadow-color, #5eb715) inset; } .btn-outline-success .btn.disabled, .btn-outline-success .btn:disabled, @@ -4027,19 +4015,19 @@ a:visited { .btn.btn-outline-success:disabled, .btn.btn-success-outline.disabled, .btn.btn-success-outline:disabled { - color: #666; - color: var(--clr-btn-success-outline-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var( - --clr-btn-success-outline-disabled-bg-color, - transparent - ); - border-color: #8c8c8c; - border-color: var(--clr-btn-success-outline-disabled-border-color, #8c8c8c); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-success-outline-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var( + --clr-btn-success-outline-disabled-bg-color, + transparent + ); + border-color: #8c8c8c; + border-color: var(--clr-btn-success-outline-disabled-border-color, #8c8c8c); + -webkit-box-shadow: none; + box-shadow: none; } .btn-danger-outline .btn, .btn-outline-danger .btn, @@ -4049,12 +4037,12 @@ a:visited { .btn.btn-outline-danger, .btn.btn-outline-warning, .btn.btn-warning-outline { - border-color: #c21d00; - border-color: var(--clr-btn-danger-outline-border-color, #c21d00); - background-color: transparent; - background-color: var(--clr-btn-danger-outline-bg-color, transparent); - color: #db2100; - color: var(--clr-btn-danger-outline-color, #db2100); + border-color: #c21d00; + border-color: var(--clr-btn-danger-outline-border-color, #c21d00); + background-color: transparent; + background-color: var(--clr-btn-danger-outline-bg-color, transparent); + color: #db2100; + color: var(--clr-btn-danger-outline-color, #db2100); } .btn-danger-outline .btn cds-icon, .btn-outline-danger .btn cds-icon, @@ -4064,8 +4052,8 @@ a:visited { .btn.btn-outline-danger cds-icon, .btn.btn-outline-warning cds-icon, .btn.btn-warning-outline cds-icon { - color: #db2100; - color: var(--clr-btn-danger-outline-color, #db2100); + color: #db2100; + color: var(--clr-btn-danger-outline-color, #db2100); } .btn-danger-outline .btn:visited, .btn-outline-danger .btn:visited, @@ -4075,8 +4063,8 @@ a:visited { .btn.btn-outline-danger:visited, .btn.btn-outline-warning:visited, .btn.btn-warning-outline:visited { - color: #db2100; - color: var(--clr-btn-danger-outline-color, #db2100); + color: #db2100; + color: var(--clr-btn-danger-outline-color, #db2100); } .btn-danger-outline .btn:hover, .btn-outline-danger .btn:hover, @@ -4086,10 +4074,10 @@ a:visited { .btn.btn-outline-danger:hover, .btn.btn-outline-warning:hover, .btn.btn-warning-outline:hover { - background-color: #feddd7; - background-color: var(--clr-btn-danger-outline-hover-bg-color, #feddd7); - color: #991700; - color: var(--clr-btn-danger-outline-hover-color, #991700); + background-color: #feddd7; + background-color: var(--clr-btn-danger-outline-hover-bg-color, #feddd7); + color: #991700; + color: var(--clr-btn-danger-outline-hover-color, #991700); } .btn-danger-outline .btn:active, .btn-outline-danger .btn:active, @@ -4099,12 +4087,12 @@ a:visited { .btn.btn-outline-danger:active, .btn.btn-outline-warning:active, .btn.btn-warning-outline:active { - -webkit-box-shadow: 0 0.1rem 0 0 #fcc5bb inset; - box-shadow: 0 0.1rem 0 0 #fcc5bb inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-danger-outline-box-shadow-color, #fcc5bb) inset; - box-shadow: 0 0.1rem 0 0 - var(--clr-btn-danger-outline-box-shadow-color, #fcc5bb) inset; + -webkit-box-shadow: 0 0.1rem 0 0 #fcc5bb inset; + box-shadow: 0 0.1rem 0 0 #fcc5bb inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-danger-outline-box-shadow-color, #fcc5bb) inset; + box-shadow: 0 0.1rem 0 0 + var(--clr-btn-danger-outline-box-shadow-color, #fcc5bb) inset; } .btn-danger-outline .btn.disabled, .btn-danger-outline .btn:disabled, @@ -4122,108 +4110,108 @@ a:visited { .btn.btn-outline-warning:disabled, .btn.btn-warning-outline.disabled, .btn.btn-warning-outline:disabled { - color: #666; - color: var(--clr-btn-danger-outline-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var( - --clr-btn-danger-outline-disabled-bg-color, - transparent - ); - border-color: #666; - border-color: var(--clr-btn-danger-outline-disabled-border-color, #666); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-danger-outline-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var( + --clr-btn-danger-outline-disabled-bg-color, + transparent + ); + border-color: #666; + border-color: var(--clr-btn-danger-outline-disabled-border-color, #666); + -webkit-box-shadow: none; + box-shadow: none; } .btn-link .btn, .btn.btn-link { - border-color: transparent; - border-color: var(--clr-btn-link-border-color, transparent); - background-color: transparent; - background-color: var(--clr-btn-link-bg-color, transparent); - color: #0072a3; - color: var(--clr-btn-link-color, #0072a3); + border-color: transparent; + border-color: var(--clr-btn-link-border-color, transparent); + background-color: transparent; + background-color: var(--clr-btn-link-bg-color, transparent); + color: #0072a3; + color: var(--clr-btn-link-color, #0072a3); } .btn-link .btn cds-icon, .btn.btn-link cds-icon { - color: #0072a3; - color: var(--clr-btn-link-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-link-color, #0072a3); } .btn-link .btn:visited, .btn.btn-link:visited { - color: #0072a3; - color: var(--clr-btn-link-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-link-color, #0072a3); } .btn-link .btn:hover, .btn.btn-link:hover { - background-color: transparent; - background-color: var(--clr-btn-link-hover-bg-color, transparent); - color: #00567a; - color: var(--clr-btn-link-hover-color, #00567a); + background-color: transparent; + background-color: var(--clr-btn-link-hover-bg-color, transparent); + color: #00567a; + color: var(--clr-btn-link-hover-color, #00567a); } .btn-link .btn:active, .btn.btn-link:active { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .btn-link .btn.disabled, .btn-link .btn:disabled, .btn.btn-link.disabled, .btn.btn-link:disabled { - color: #666; - color: var(--clr-btn-link-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var(--clr-btn-link-disabled-bg-color, transparent); - border-color: transparent; - border-color: var(--clr-btn-link-disabled-border-color, transparent); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-link-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var(--clr-btn-link-disabled-bg-color, transparent); + border-color: transparent; + border-color: var(--clr-btn-link-disabled-border-color, transparent); + -webkit-box-shadow: none; + box-shadow: none; } .alert-app-level .alert-item .btn, .btn-inverse .btn, .btn.btn-inverse { - border-color: #fff; - border-color: var(--clr-btn-inverse-border-color, #fff); - background-color: transparent; - background-color: var(--clr-btn-inverse-bg-color, transparent); - color: #fff; - color: var(--clr-btn-inverse-color, #fff); + border-color: #fff; + border-color: var(--clr-btn-inverse-border-color, #fff); + background-color: transparent; + background-color: var(--clr-btn-inverse-bg-color, transparent); + color: #fff; + color: var(--clr-btn-inverse-color, #fff); } .alert-app-level .alert-item .btn cds-icon, .btn-inverse .btn cds-icon, .btn.btn-inverse cds-icon { - color: #fff; - color: var(--clr-btn-inverse-color, #fff); + color: #fff; + color: var(--clr-btn-inverse-color, #fff); } .alert-app-level .alert-item .btn:visited, .btn-inverse .btn:visited, .btn.btn-inverse:visited { - color: #fff; - color: var(--clr-btn-inverse-color, #fff); + color: #fff; + color: var(--clr-btn-inverse-color, #fff); } .alert-app-level .alert-item .btn:hover, .btn-inverse .btn:hover, .btn.btn-inverse:hover { - background-color: rgba(255, 255, 255, 0.15); - background-color: var( - --clr-btn-inverse-hover-bg-color, - rgba(255, 255, 255, 0.15) - ); - color: #fff; - color: var(--clr-btn-inverse-hover-color, #fff); + background-color: rgba(255, 255, 255, 0.15); + background-color: var( + --clr-btn-inverse-hover-bg-color, + rgba(255, 255, 255, 0.15) + ); + color: #fff; + color: var(--clr-btn-inverse-hover-color, #fff); } .alert-app-level .alert-item .btn:active, .btn-inverse .btn:active, .btn.btn-inverse:active { - -webkit-box-shadow: 0 0.1rem 0 0 rgba(0, 0, 0, 0.25) inset; - box-shadow: 0 0.1rem 0 0 rgba(0, 0, 0, 0.25) inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-inverse-box-shadow-color, rgba(0, 0, 0, 0.25)) inset; - box-shadow: 0 0.1rem 0 0 - var(--clr-btn-inverse-box-shadow-color, rgba(0, 0, 0, 0.25)) inset; + -webkit-box-shadow: 0 0.1rem 0 0 rgba(0, 0, 0, 0.25) inset; + box-shadow: 0 0.1rem 0 0 rgba(0, 0, 0, 0.25) inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-inverse-box-shadow-color, rgba(0, 0, 0, 0.25)) inset; + box-shadow: 0 0.1rem 0 0 + var(--clr-btn-inverse-box-shadow-color, rgba(0, 0, 0, 0.25)) inset; } .alert-app-level .alert-item .btn.disabled, .alert-app-level .alert-item .btn:disabled, @@ -4231,114 +4219,114 @@ a:visited { .btn-inverse .btn:disabled, .btn.btn-inverse.disabled, .btn.btn-inverse:disabled { - color: #fff; - color: var(--clr-btn-inverse-disabled-color, #fff); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var(--clr-btn-inverse-disabled-bg-color, transparent); - border-color: #fff; - border-color: var(--clr-btn-inverse-disabled-border-color, #fff); - -webkit-box-shadow: none; - box-shadow: none; + color: #fff; + color: var(--clr-btn-inverse-disabled-color, #fff); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var(--clr-btn-inverse-disabled-bg-color, transparent); + border-color: #fff; + border-color: var(--clr-btn-inverse-disabled-border-color, #fff); + -webkit-box-shadow: none; + box-shadow: none; } .alert-app-level .alert-item .btn, .btn-sm .btn, .btn.btn-sm { - line-height: 1.15rem; - line-height: var(--clr-btn-appearance-standard-line-height, 1.15rem); - letter-spacing: 0.073em; - font-size: 0.55rem; - font-size: var(--clr-btn-appearance-standard-font-size, 0.55rem); - font-weight: 500; - font-weight: var(--clr-btn-appearance-standard-font-weight, 500); - height: 1.2rem; - height: var(--clr-btn-appearance-standard-height, 1.2rem); - padding: 0 0.6rem; - padding: var(--clr-btn-appearance-standard-padding, 0 0.6rem); + line-height: 1.15rem; + line-height: var(--clr-btn-appearance-standard-line-height, 1.15rem); + letter-spacing: 0.073em; + font-size: 0.55rem; + font-size: var(--clr-btn-appearance-standard-font-size, 0.55rem); + font-weight: 500; + font-weight: var(--clr-btn-appearance-standard-font-weight, 500); + height: 1.2rem; + height: var(--clr-btn-appearance-standard-height, 1.2rem); + padding: 0 0.6rem; + padding: var(--clr-btn-appearance-standard-padding, 0 0.6rem); } .btn-block { - display: block; - width: 100%; - max-width: 100%; + display: block; + width: 100%; + max-width: 100%; } .btn { - margin-top: 0.3rem; - margin-top: var(--clr-btn-vertical-margin, 0.3rem); - margin-bottom: 0.3rem; - margin-bottom: var(--clr-btn-vertical-margin, 0.3rem); - margin-right: 0.6rem; - margin-right: var(--clr-btn-horizontal-margin, 0.6rem); - margin-left: 0; + margin-top: 0.3rem; + margin-top: var(--clr-btn-vertical-margin, 0.3rem); + margin-bottom: 0.3rem; + margin-bottom: var(--clr-btn-vertical-margin, 0.3rem); + margin-right: 0.6rem; + margin-right: var(--clr-btn-horizontal-margin, 0.6rem); + margin-left: 0; } .btn.btn-link { - margin-right: 0; + margin-right: 0; } .alert-app-level .alert-item .btn.btn-link, .btn.btn-link.btn-inverse { - border-color: transparent; + border-color: transparent; } .alert-app-level .alert-item .btn:not(.btn-link) cds-icon, .btn-sm:not(.btn-link) cds-icon { - width: 0.6rem; - width: var(--clr-btn-appearance-standard-icon-size, 0.6rem); - height: 0.6rem; - height: var(--clr-btn-appearance-standard-icon-size, 0.6rem); - -webkit-transform: translate3d(0, -0.05rem, 0); - transform: translate3d(0, -0.05rem, 0); + width: 0.6rem; + width: var(--clr-btn-appearance-standard-icon-size, 0.6rem); + height: 0.6rem; + height: var(--clr-btn-appearance-standard-icon-size, 0.6rem); + -webkit-transform: translate3d(0, -0.05rem, 0); + transform: translate3d(0, -0.05rem, 0); } .btn-icon { - min-width: 0; + min-width: 0; } .btn-icon.disabled, .btn-icon:disabled { - color: #666; - color: var(--clr-btn-icon-disabled-color, #666); + color: #666; + color: var(--clr-btn-icon-disabled-color, #666); } .btn-group.btn-danger .dropdown-toggle, .btn-group.btn-primary .dropdown-toggle, .btn-group.btn-success .dropdown-toggle, .btn-group.btn-warning .dropdown-toggle { - border-color: #0072a3; - border-color: var(--clr-btn-primary-border-color, #0072a3); - background-color: #0072a3; - background-color: var(--clr-btn-primary-bg-color, #0072a3); - color: #fff; - color: var(--clr-btn-primary-color, #fff); + border-color: #0072a3; + border-color: var(--clr-btn-primary-border-color, #0072a3); + background-color: #0072a3; + background-color: var(--clr-btn-primary-bg-color, #0072a3); + color: #fff; + color: var(--clr-btn-primary-color, #fff); } .btn-group.btn-danger .dropdown-toggle cds-icon, .btn-group.btn-primary .dropdown-toggle cds-icon, .btn-group.btn-success .dropdown-toggle cds-icon, .btn-group.btn-warning .dropdown-toggle cds-icon { - color: #fff; - color: var(--clr-btn-primary-color, #fff); + color: #fff; + color: var(--clr-btn-primary-color, #fff); } .btn-group.btn-danger .dropdown-toggle:visited, .btn-group.btn-primary .dropdown-toggle:visited, .btn-group.btn-success .dropdown-toggle:visited, .btn-group.btn-warning .dropdown-toggle:visited { - color: #fff; - color: var(--clr-btn-primary-color, #fff); + color: #fff; + color: var(--clr-btn-primary-color, #fff); } .btn-group.btn-danger .dropdown-toggle:hover, .btn-group.btn-primary .dropdown-toggle:hover, .btn-group.btn-success .dropdown-toggle:hover, .btn-group.btn-warning .dropdown-toggle:hover { - background-color: #00567a; - background-color: var(--clr-btn-primary-hover-bg-color, #00567a); - color: #e3f5fc; - color: var(--clr-btn-primary-hover-color, #e3f5fc); + background-color: #00567a; + background-color: var(--clr-btn-primary-hover-bg-color, #00567a); + color: #e3f5fc; + color: var(--clr-btn-primary-hover-color, #e3f5fc); } .btn-group.btn-danger .dropdown-toggle:active, .btn-group.btn-primary .dropdown-toggle:active, .btn-group.btn-success .dropdown-toggle:active, .btn-group.btn-warning .dropdown-toggle:active { - -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; - box-shadow: 0 0.1rem 0 0 #179bd3 inset; - -webkit-box-shadow: 0 0.1rem 0 0 - var(--clr-btn-primary-box-shadow-color, #179bd3) inset; - box-shadow: 0 0.1rem 0 0 var(--clr-btn-primary-box-shadow-color, #179bd3) - inset; + -webkit-box-shadow: 0 0.1rem 0 0 #179bd3 inset; + box-shadow: 0 0.1rem 0 0 #179bd3 inset; + -webkit-box-shadow: 0 0.1rem 0 0 + var(--clr-btn-primary-box-shadow-color, #179bd3) inset; + box-shadow: 0 0.1rem 0 0 var(--clr-btn-primary-box-shadow-color, #179bd3) + inset; } .btn-group.btn-danger .dropdown-toggle.disabled, .btn-group.btn-danger .dropdown-toggle:disabled, @@ -4348,277 +4336,277 @@ a:visited { .btn-group.btn-success .dropdown-toggle:disabled, .btn-group.btn-warning .dropdown-toggle.disabled, .btn-group.btn-warning .dropdown-toggle:disabled { - color: #666; - color: var(--clr-btn-primary-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: #ccc; - background-color: var(--clr-btn-primary-disabled-bg-color, #ccc); - border-color: #ccc; - border-color: var(--clr-btn-primary-disabled-border-color, #ccc); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-primary-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: #ccc; + background-color: var(--clr-btn-primary-disabled-bg-color, #ccc); + border-color: #ccc; + border-color: var(--clr-btn-primary-disabled-border-color, #ccc); + -webkit-box-shadow: none; + box-shadow: none; } .btn-group.btn-link .dropdown-toggle { - border-color: transparent; - border-color: var(--clr-btn-link-border-color, transparent); - background-color: transparent; - background-color: var(--clr-btn-link-bg-color, transparent); - color: #0072a3; - color: var(--clr-btn-link-color, #0072a3); + border-color: transparent; + border-color: var(--clr-btn-link-border-color, transparent); + background-color: transparent; + background-color: var(--clr-btn-link-bg-color, transparent); + color: #0072a3; + color: var(--clr-btn-link-color, #0072a3); } .btn-group.btn-link .dropdown-toggle cds-icon { - color: #0072a3; - color: var(--clr-btn-link-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-link-color, #0072a3); } .btn-group.btn-link .dropdown-toggle:visited { - color: #0072a3; - color: var(--clr-btn-link-color, #0072a3); + color: #0072a3; + color: var(--clr-btn-link-color, #0072a3); } .btn-group.btn-link .dropdown-toggle:hover { - background-color: transparent; - background-color: var(--clr-btn-link-hover-bg-color, transparent); - color: #00567a; - color: var(--clr-btn-link-hover-color, #00567a); + background-color: transparent; + background-color: var(--clr-btn-link-hover-bg-color, transparent); + color: #00567a; + color: var(--clr-btn-link-hover-color, #00567a); } .btn-group.btn-link .dropdown-toggle:active { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .btn-group.btn-link .dropdown-toggle.disabled, .btn-group.btn-link .dropdown-toggle:disabled { - color: #666; - color: var(--clr-btn-link-disabled-color, #666); - cursor: not-allowed; - opacity: 0.4; - background-color: transparent; - background-color: var(--clr-btn-link-disabled-bg-color, transparent); - border-color: transparent; - border-color: var(--clr-btn-link-disabled-border-color, transparent); - -webkit-box-shadow: none; - box-shadow: none; + color: #666; + color: var(--clr-btn-link-disabled-color, #666); + cursor: not-allowed; + opacity: 0.4; + background-color: transparent; + background-color: var(--clr-btn-link-disabled-bg-color, transparent); + border-color: transparent; + border-color: var(--clr-btn-link-disabled-border-color, transparent); + -webkit-box-shadow: none; + box-shadow: none; } .alert-app-level - .alert-item - .btn-group.btn - .btn-group-overflow - > .dropdown-toggle, + .alert-item + .btn-group.btn + .btn-group-overflow + > .dropdown-toggle, .btn-group.btn-sm .btn-group-overflow > .dropdown-toggle { - line-height: 1.15rem; - line-height: var(--clr-btn-appearance-standard-line-height, 1.15rem); - letter-spacing: 0.073em; - font-size: 0.55rem; - font-size: var(--clr-btn-appearance-standard-font-size, 0.55rem); - font-weight: 500; - font-weight: var(--clr-btn-appearance-standard-font-weight, 500); - height: 1.2rem; - height: var(--clr-btn-appearance-standard-height, 1.2rem); - padding: 0 0.6rem; - padding: var(--clr-btn-appearance-standard-padding, 0 0.6rem); + line-height: 1.15rem; + line-height: var(--clr-btn-appearance-standard-line-height, 1.15rem); + letter-spacing: 0.073em; + font-size: 0.55rem; + font-size: var(--clr-btn-appearance-standard-font-size, 0.55rem); + font-weight: 500; + font-weight: var(--clr-btn-appearance-standard-font-weight, 500); + height: 1.2rem; + height: var(--clr-btn-appearance-standard-height, 1.2rem); + padding: 0 0.6rem; + padding: var(--clr-btn-appearance-standard-padding, 0 0.6rem); } .checkbox-inline.btn, .checkbox.btn, .radio-inline.btn, .radio.btn { - padding: 0; + padding: 0; } .checkbox-inline.btn label, .checkbox.btn label, .radio-inline.btn label, .radio.btn label { - display: block; - line-height: inherit; - padding: 0 0.6rem; - cursor: pointer; -} -.checkbox-inline.btn input[type='checkbox'] + label::after, -.checkbox-inline.btn input[type='checkbox'] + label::before, -.checkbox.btn input[type='checkbox'] + label::after, -.checkbox.btn input[type='checkbox'] + label::before { - content: none; -} -.radio-inline.btn input[type='radio'] + label::after, -.radio-inline.btn input[type='radio'] + label::before, -.radio.btn input[type='radio'] + label::after, -.radio.btn input[type='radio'] + label::before { - content: none; -} -.checkbox-inline.btn input[type='checkbox']:checked + label, -.checkbox.btn input[type='checkbox']:checked + label { - background-color: #0072a3; - color: #fff; + display: block; + line-height: inherit; + padding: 0 0.6rem; + cursor: pointer; +} +.checkbox-inline.btn input[type="checkbox"] + label::after, +.checkbox-inline.btn input[type="checkbox"] + label::before, +.checkbox.btn input[type="checkbox"] + label::after, +.checkbox.btn input[type="checkbox"] + label::before { + content: none; +} +.radio-inline.btn input[type="radio"] + label::after, +.radio-inline.btn input[type="radio"] + label::before, +.radio.btn input[type="radio"] + label::after, +.radio.btn input[type="radio"] + label::before { + content: none; +} +.checkbox-inline.btn input[type="checkbox"]:checked + label, +.checkbox.btn input[type="checkbox"]:checked + label { + background-color: #0072a3; + color: #fff; } .checkbox-inline.btn label, .checkbox.btn label { - width: 100%; + width: 100%; } -.checkbox-inline.btn.btn-info input[type='checkbox']:checked + label, -.checkbox-inline.btn.btn-info-outline input[type='checkbox']:checked + label, -.checkbox-inline.btn.btn-outline input[type='checkbox']:checked + label, -.checkbox-inline.btn.btn-outline-info input[type='checkbox']:checked + label, -.checkbox-inline.btn.btn-outline-primary input[type='checkbox']:checked + label, +.checkbox-inline.btn.btn-info input[type="checkbox"]:checked + label, +.checkbox-inline.btn.btn-info-outline input[type="checkbox"]:checked + label, +.checkbox-inline.btn.btn-outline input[type="checkbox"]:checked + label, +.checkbox-inline.btn.btn-outline-info input[type="checkbox"]:checked + label, +.checkbox-inline.btn.btn-outline-primary input[type="checkbox"]:checked + label, .checkbox-inline.btn.btn-outline-secondary - input[type='checkbox']:checked - + label, -.checkbox-inline.btn.btn-primary-outline input[type='checkbox']:checked + label, -.checkbox-inline.btn.btn-secondary input[type='checkbox']:checked + label, + input[type="checkbox"]:checked + + label, +.checkbox-inline.btn.btn-primary-outline input[type="checkbox"]:checked + label, +.checkbox-inline.btn.btn-secondary input[type="checkbox"]:checked + label, .checkbox-inline.btn.btn-secondary-outline - input[type='checkbox']:checked - + label, -.checkbox.btn.btn-info input[type='checkbox']:checked + label, -.checkbox.btn.btn-info-outline input[type='checkbox']:checked + label, -.checkbox.btn.btn-outline input[type='checkbox']:checked + label, -.checkbox.btn.btn-outline-info input[type='checkbox']:checked + label, -.checkbox.btn.btn-outline-primary input[type='checkbox']:checked + label, -.checkbox.btn.btn-outline-secondary input[type='checkbox']:checked + label, -.checkbox.btn.btn-primary-outline input[type='checkbox']:checked + label, -.checkbox.btn.btn-secondary input[type='checkbox']:checked + label, -.checkbox.btn.btn-secondary-outline input[type='checkbox']:checked + label { - background-color: #0072a3; - color: #fff; -} -.checkbox-inline.btn.btn-primary input[type='checkbox']:checked + label, -.checkbox.btn.btn-primary input[type='checkbox']:checked + label { - background-color: #0072a3; - color: #fff; -} -.checkbox-inline.btn.btn-success input[type='checkbox']:checked + label, -.checkbox.btn.btn-success input[type='checkbox']:checked + label { - background-color: #306b00; - color: #fff; -} -.checkbox-inline.btn.btn-danger input[type='checkbox']:checked + label, -.checkbox-inline.btn.btn-warning input[type='checkbox']:checked + label, -.checkbox.btn.btn-danger input[type='checkbox']:checked + label, -.checkbox.btn.btn-warning input[type='checkbox']:checked + label { - background-color: #c21d00; - color: #fff; -} -.checkbox-inline.btn.btn-link input[type='checkbox']:checked + label, -.checkbox.btn.btn-link input[type='checkbox']:checked + label { - background-color: transparent; - color: #00567a; + input[type="checkbox"]:checked + + label, +.checkbox.btn.btn-info input[type="checkbox"]:checked + label, +.checkbox.btn.btn-info-outline input[type="checkbox"]:checked + label, +.checkbox.btn.btn-outline input[type="checkbox"]:checked + label, +.checkbox.btn.btn-outline-info input[type="checkbox"]:checked + label, +.checkbox.btn.btn-outline-primary input[type="checkbox"]:checked + label, +.checkbox.btn.btn-outline-secondary input[type="checkbox"]:checked + label, +.checkbox.btn.btn-primary-outline input[type="checkbox"]:checked + label, +.checkbox.btn.btn-secondary input[type="checkbox"]:checked + label, +.checkbox.btn.btn-secondary-outline input[type="checkbox"]:checked + label { + background-color: #0072a3; + color: #fff; +} +.checkbox-inline.btn.btn-primary input[type="checkbox"]:checked + label, +.checkbox.btn.btn-primary input[type="checkbox"]:checked + label { + background-color: #0072a3; + color: #fff; +} +.checkbox-inline.btn.btn-success input[type="checkbox"]:checked + label, +.checkbox.btn.btn-success input[type="checkbox"]:checked + label { + background-color: #306b00; + color: #fff; +} +.checkbox-inline.btn.btn-danger input[type="checkbox"]:checked + label, +.checkbox-inline.btn.btn-warning input[type="checkbox"]:checked + label, +.checkbox.btn.btn-danger input[type="checkbox"]:checked + label, +.checkbox.btn.btn-warning input[type="checkbox"]:checked + label { + background-color: #c21d00; + color: #fff; +} +.checkbox-inline.btn.btn-link input[type="checkbox"]:checked + label, +.checkbox.btn.btn-link input[type="checkbox"]:checked + label { + background-color: transparent; + color: #00567a; } .alert-app-level - .alert-item - .checkbox-inline.btn - input[type='checkbox']:checked - + label, + .alert-item + .checkbox-inline.btn + input[type="checkbox"]:checked + + label, .alert-app-level - .alert-item - .checkbox.btn - input[type='checkbox']:checked - + label, -.checkbox-inline.btn.btn-inverse input[type='checkbox']:checked + label, -.checkbox.btn.btn-inverse input[type='checkbox']:checked + label { - background-color: rgba(255, 255, 255, 0.15); - color: #fff; -} -.radio.btn input[type='radio']:checked + label { - background-color: #0072a3; - color: #fff; + .alert-item + .checkbox.btn + input[type="checkbox"]:checked + + label, +.checkbox-inline.btn.btn-inverse input[type="checkbox"]:checked + label, +.checkbox.btn.btn-inverse input[type="checkbox"]:checked + label { + background-color: rgba(255, 255, 255, 0.15); + color: #fff; +} +.radio.btn input[type="radio"]:checked + label { + background-color: #0072a3; + color: #fff; } .radio.btn label { - width: 100%; -} -.radio.btn.btn-info input[type='radio']:checked + label, -.radio.btn.btn-info-outline input[type='radio']:checked + label, -.radio.btn.btn-outline input[type='radio']:checked + label, -.radio.btn.btn-outline-info input[type='radio']:checked + label, -.radio.btn.btn-outline-primary input[type='radio']:checked + label, -.radio.btn.btn-outline-secondary input[type='radio']:checked + label, -.radio.btn.btn-primary-outline input[type='radio']:checked + label, -.radio.btn.btn-secondary input[type='radio']:checked + label, -.radio.btn.btn-secondary-outline input[type='radio']:checked + label { - background-color: #0072a3; - color: #fff; -} -.radio.btn.btn-primary input[type='radio']:checked + label { - background-color: #0072a3; - color: #fff; -} -.radio.btn.btn-success input[type='radio']:checked + label { - background-color: #306b00; - color: #fff; -} -.radio.btn.btn-danger input[type='radio']:checked + label, -.radio.btn.btn-warning input[type='radio']:checked + label { - background-color: #c21d00; - color: #fff; -} -.radio.btn.btn-outline-success input[type='radio']:checked + label, -.radio.btn.btn-success-outline input[type='radio']:checked + label { - background-color: #3c8500; - color: #fff; -} -.radio.btn.btn-danger-outline input[type='radio']:checked + label, -.radio.btn.btn-outline-danger input[type='radio']:checked + label, -.radio.btn.btn-outline-warning input[type='radio']:checked + label, -.radio.btn.btn-warning-outline input[type='radio']:checked + label { - background-color: #c21d00; - color: #fff; -} -.radio.btn.btn-link input[type='radio']:checked + label { - background-color: transparent; - color: #00567a; -} -.alert-app-level .alert-item .radio.btn input[type='radio']:checked + label, -.radio.btn.btn-inverse input[type='radio']:checked + label { - background-color: rgba(255, 255, 255, 0.15); - color: #fff; + width: 100%; +} +.radio.btn.btn-info input[type="radio"]:checked + label, +.radio.btn.btn-info-outline input[type="radio"]:checked + label, +.radio.btn.btn-outline input[type="radio"]:checked + label, +.radio.btn.btn-outline-info input[type="radio"]:checked + label, +.radio.btn.btn-outline-primary input[type="radio"]:checked + label, +.radio.btn.btn-outline-secondary input[type="radio"]:checked + label, +.radio.btn.btn-primary-outline input[type="radio"]:checked + label, +.radio.btn.btn-secondary input[type="radio"]:checked + label, +.radio.btn.btn-secondary-outline input[type="radio"]:checked + label { + background-color: #0072a3; + color: #fff; +} +.radio.btn.btn-primary input[type="radio"]:checked + label { + background-color: #0072a3; + color: #fff; +} +.radio.btn.btn-success input[type="radio"]:checked + label { + background-color: #306b00; + color: #fff; +} +.radio.btn.btn-danger input[type="radio"]:checked + label, +.radio.btn.btn-warning input[type="radio"]:checked + label { + background-color: #c21d00; + color: #fff; +} +.radio.btn.btn-outline-success input[type="radio"]:checked + label, +.radio.btn.btn-success-outline input[type="radio"]:checked + label { + background-color: #3c8500; + color: #fff; +} +.radio.btn.btn-danger-outline input[type="radio"]:checked + label, +.radio.btn.btn-outline-danger input[type="radio"]:checked + label, +.radio.btn.btn-outline-warning input[type="radio"]:checked + label, +.radio.btn.btn-warning-outline input[type="radio"]:checked + label { + background-color: #c21d00; + color: #fff; +} +.radio.btn.btn-link input[type="radio"]:checked + label { + background-color: transparent; + color: #00567a; +} +.alert-app-level .alert-item .radio.btn input[type="radio"]:checked + label, +.radio.btn.btn-inverse input[type="radio"]:checked + label { + background-color: rgba(255, 255, 255, 0.15); + color: #fff; } .btn-group { - display: -webkit-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; - margin-right: 0.6rem; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + margin-right: 0.6rem; } .btn-group .btn { - margin: 0; - vertical-align: top; - overflow: visible; + margin: 0; + vertical-align: top; + overflow: visible; } .btn-group .btn label { - height: 100%; -} -.btn-group .btn input[type='checkbox']:focus, -.btn-group .btn input[type='radio']:focus { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - opacity: 1; - top: 0; - height: 100%; - width: 100%; - -webkit-box-shadow: 0 0 0.25rem #51cbee; - box-shadow: 0 0 0.25rem #51cbee; - -webkit-box-shadow: 0 0 0.25rem var(--clr-btn-group-focus-outline, #51cbee); - box-shadow: 0 0 0.25rem var(--clr-btn-group-focus-outline, #51cbee); - padding: 0.15rem 0 0.15rem 0.15rem; - border-width: 0.05rem; - border-style: solid; - border-color: #51cbee; - border-color: var(--clr-btn-group-focus-outline, #51cbee); -} -.btn-group .btn input[type='checkbox']:focus::-ms-check, -.btn-group .btn input[type='radio']:focus::-ms-check { - display: none; + height: 100%; +} +.btn-group .btn input[type="checkbox"]:focus, +.btn-group .btn input[type="radio"]:focus { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + opacity: 1; + top: 0; + height: 100%; + width: 100%; + -webkit-box-shadow: 0 0 0.25rem #51cbee; + box-shadow: 0 0 0.25rem #51cbee; + -webkit-box-shadow: 0 0 0.25rem var(--clr-btn-group-focus-outline, #51cbee); + box-shadow: 0 0 0.25rem var(--clr-btn-group-focus-outline, #51cbee); + padding: 0.15rem 0 0.15rem 0.15rem; + border-width: 0.05rem; + border-style: solid; + border-color: #51cbee; + border-color: var(--clr-btn-group-focus-outline, #51cbee); +} +.btn-group .btn input[type="checkbox"]:focus::-ms-check, +.btn-group .btn input[type="radio"]:focus::-ms-check { + display: none; } .btn-group .btn:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } .btn-group .btn:not(:last-child) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } .btn-group .tooltip:not(:first-child) .btn { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } .btn-group .tooltip:not(:last-child) .btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } .btn-group.btn-danger .btn:not(:last-child), .btn-group.btn-danger .tooltip:not(:last-child) .btn, @@ -4628,392 +4616,377 @@ a:visited { .btn-group.btn-success .tooltip:not(:last-child) .btn, .btn-group.btn-warning .btn:not(:last-child), .btn-group.btn-warning .tooltip:not(:last-child) .btn { - margin: 0; - margin-right: 0.05rem; - margin-right: var(--clr-btn-border-width, 0.05rem); + margin: 0; + margin-right: 0.05rem; + margin-right: var(--clr-btn-border-width, 0.05rem); } .btn-group.btn-danger .dropdown-menu .btn, .btn-group.btn-primary .dropdown-menu .btn, .btn-group.btn-success .dropdown-menu .btn, .btn-group.btn-warning .dropdown-menu .btn { - margin: 0; + margin: 0; } .btn-group > .btn-group-overflow { - position: relative; + position: relative; } .btn-group - > .btn-group-overflow:last-child:not(:first-child) - > .btn:first-child { - border-radius: 0.15rem; - border-radius: var(--clr-btn-border-radius, 0.15rem); - border-top-left-radius: 0; - border-bottom-left-radius: 0; + > .btn-group-overflow:last-child:not(:first-child) + > .btn:first-child { + border-radius: 0.15rem; + border-radius: var(--clr-btn-border-radius, 0.15rem); + border-top-left-radius: 0; + border-bottom-left-radius: 0; } .btn-group > .btn-group-overflow:last-child:first-child > .btn:first-child { - border-radius: 0.15rem; - border-radius: var(--clr-btn-border-radius, 0.15rem); + border-radius: 0.15rem; + border-radius: var(--clr-btn-border-radius, 0.15rem); } .btn-group .btn + .btn { - border-left: none; + border-left: none; } .btn-group .tooltip:not(:first-child) .btn { - border-left: none; + border-left: none; } .btn-group .btn + .btn-group-overflow .btn { - border-left: none; + border-left: none; } .btn-group.btn-link .dropdown-toggle { - min-width: 0; + min-width: 0; } .btn-group.btn-icon-link.btn-link .btn { - min-width: 0; + min-width: 0; } .btn-group.btn-icon .btn { - min-width: 0; + min-width: 0; } .btn-group .clr-icon-title { - display: none; - text-transform: none; + display: none; + text-transform: none; } .btn-group .dropdown-toggle { - display: block; + display: block; } .btn-group .dropdown-menu cds-icon { - display: none; + display: none; } .btn-group .dropdown-menu .clr-icon-title { - display: inline; + display: inline; } .checkbox, .radio { - position: relative; -} -.checkbox input[type='checkbox'], -.checkbox input[type='radio'], -.radio input[type='checkbox'], -.radio input[type='radio'] { - position: absolute; - top: 0; - left: 0; - height: 0; - width: 0; - opacity: 0; + position: relative; +} +.checkbox input[type="checkbox"], +.checkbox input[type="radio"], +.radio input[type="checkbox"], +.radio input[type="radio"] { + position: absolute; + top: 0; + left: 0; + height: 0; + width: 0; + opacity: 0; } .card-footer .checkbox.btn label, .card-footer .radio.btn label { - line-height: 1.15rem; + line-height: 1.15rem; } .dropdown-menu.clr-button-group-menu { - visibility: visible; + visibility: visible; } button.close { - padding: 0; - cursor: pointer; - background: 0 0; - border: 0; - -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; + -webkit-appearance: none; } .close { - float: right; - font-size: 1.8rem; - -webkit-transition: color linear 0.2s; - transition: color linear 0.2s; - font-weight: 200; - text-shadow: none; - line-height: inherit; - color: #8c8c8c; - color: var(--clr-close-color--normal, #8c8c8c); + float: right; + font-size: 1.8rem; + -webkit-transition: color linear 0.2s; + transition: color linear 0.2s; + font-weight: 200; + text-shadow: none; + line-height: inherit; + color: #8c8c8c; + color: var(--clr-close-color--normal, #8c8c8c); } .close cds-icon { - fill: #8c8c8c; - fill: var(--clr-close-color--normal, #8c8c8c); + fill: #8c8c8c; + fill: var(--clr-close-color--normal, #8c8c8c); } .close:active, .close:focus, .close:hover { - opacity: 1; - color: #000; - color: var(--clr-close-color--hover, #000); + opacity: 1; + color: #000; + color: var(--clr-close-color--hover, #000); } .close:active cds-icon, .close:focus cds-icon, .close:hover cds-icon { - fill: #000; - fill: var(--clr-close-color--hover, #000); + fill: #000; + fill: var(--clr-close-color--hover, #000); } .close:focus { - outline: 0; - -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + outline: 0; + -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + box-shadow: 0 0 0.1rem 0.1rem #69c0e2; } :root { - --clr-alert-action-color: var(--clr-color-neutral-700); - --clr-alert-action-active-color: var(--clr-color-secondary-action-900); - --clr-alert-close-icon-color: var(--clr-alert-action-color); - --clr-alert-close-icon-hover-color: var(--clr-alert-action-active-color); - --clr-alert-close-icon-opacity: 1; - --clr-alert-close-icon-hover-opacity: 1; - --clr-app-level-alert-color: var(--clr-color-neutral-0); - --clr-app-alert-close-icon-color: var(--clr-app-level-alert-color); - --clr-app-alert-close-icon-opacity: 0.8; - --clr-app-alert-close-icon-hover-opacity: 1; - --clr-alert-borderradius: var(--clr-global-borderradius); - --clr-alert-info-bg-color: var(--clr-color-action-50); - --clr-alert-info-font-color: var(--clr-color-neutral-700); - --clr-alert-info-border-color: var(--clr-color-action-800); - --clr-alert-info-icon-color: var(--clr-color-action-800); - --clr-alert-info-action-color: var(--clr-alert-action-color); - --clr-alert-info-action-active-color: var(--clr-alert-action-active-color); - --clr-alert-info-close-icon-color: var(--clr-alert-close-icon-color); - --clr-alert-info-close-icon-opacity: var(--clr-alert-close-icon-opacity); - --clr-alert-info-close-icon-hover-color: var( - --clr-alert-close-icon-hover-color - ); - --clr-alert-info-close-icon-hover-opacity: var( - --clr-alert-close-icon-hover-opacity - ); - --clr-alert-success-bg-color: var(--clr-color-success-50); - --clr-alert-success-font-color: var(--clr-color-neutral-700); - --clr-alert-success-border-color: var(--clr-color-success-800); - --clr-alert-success-icon-color: var(--clr-color-success-800); - --clr-alert-success-action-color: var(--clr-alert-action-color); - --clr-alert-success-action-active-color: var( - --clr-alert-action-active-color - ); - --clr-alert-success-close-icon-color: var(--clr-alert-close-icon-color); - --clr-alert-success-close-icon-opacity: var(--clr-alert-close-icon-opacity); - --clr-alert-success-close-icon-hover-color: var( - --clr-alert-close-icon-hover-color - ); - --clr-alert-success-close-icon-hover-opacity: var( - --clr-alert-close-icon-hover-opacity - ); - --clr-alert-warning-bg-color: var(--clr-color-warning-100); - --clr-alert-warning-font-color: var(--clr-color-neutral-900); - --clr-alert-warning-border-color: var(--clr-color-warning-800); - --clr-alert-warning-icon-color: var(--clr-color-warning-800); - --clr-alert-warning-action-color: var(--clr-alert-action-color); - --clr-alert-warning-action-active-color: var( - --clr-alert-action-active-color - ); - --clr-alert-warning-close-icon-color: var(--clr-alert-close-icon-color); - --clr-alert-warning-close-icon-opacity: var(--clr-alert-close-icon-opacity); - --clr-alert-warning-close-icon-hover-color: var( - --clr-alert-close-icon-hover-color - ); - --clr-alert-warning-close-icon-hover-opacity: var( - --clr-alert-close-icon-hover-opacity - ); - --clr-alert-danger-bg-color: var(--clr-color-danger-100); - --clr-alert-danger-font-color: var(--clr-color-neutral-700); - --clr-alert-danger-border-color: var(--clr-color-danger-900); - --clr-alert-danger-icon-color: var(--clr-color-danger-900); - --clr-alert-danger-action-color: var(--clr-alert-action-color); - --clr-alert-danger-action-active-color: var( - --clr-alert-action-active-color - ); - --clr-alert-danger-close-icon-color: var(--clr-alert-close-icon-color); - --clr-alert-danger-close-icon-opacity: var(--clr-alert-close-icon-opacity); - --clr-alert-danger-close-icon-hover-color: var( - --clr-alert-close-icon-hover-color - ); - --clr-alert-danger-close-icon-hover-opacity: var( - --clr-alert-close-icon-hover-opacity - ); - --clr-app-alert-info-bg-color: var(--clr-color-action-600); - --clr-app-alert-info-font-color: var(--clr-app-level-alert-color); - --clr-app-alert-info-border-color: none; - --clr-app-alert-info-icon-color: var(--clr-app-alert-close-icon-color); - --clr-app-alert-info-action-color: var(--clr-app-alert-info-font-color); - --clr-app-alert-info-action-active-color: var( - --clr-app-alert-info-font-color - ); - --clr-app-alert-info-close-icon-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-info-close-icon-opacity: var( - --clr-app-alert-close-icon-opacity - ); - --clr-app-alert-info-close-icon-hover-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-info-close-icon-hover-opacity: var( - --clr-app-alert-close-icon-hover-opacity - ); - --clr-app-alert-warning-bg-color: hsl(26, 100%, 38%); - --clr-app-alert-warning-border-color: none; - --clr-app-alert-warning-icon-color: var(--clr-app-alert-close-icon-color); - --clr-app-alert-warning-font-color: var(--clr-app-level-alert-color); - --clr-app-alert-warning-close-icon-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-warning-action-color: var( - --clr-app-alert-warning-font-color - ); - --clr-app-alert-warning-action-active-color: var( - --clr-app-alert-warning-font-color - ); - --clr-app-alert-warning-close-icon-opacity: var( - --clr-app-alert-close-icon-opacity - ); - --clr-app-alert-warning-close-icon-hover-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-warning-close-icon-hover-opacity: var( - --clr-app-alert-close-icon-hover-opacity - ); - --clr-app-alert-danger-bg-color: var(--clr-color-danger-800); - --clr-app-alert-danger-border-color: none; - --clr-app-alert-danger-icon-color: var(--clr-app-alert-close-icon-color); - --clr-app-alert-danger-font-color: var(--clr-app-level-alert-color); - --clr-app-alert-danger-close-icon-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-danger-action-color: var(--clr-app-alert-danger-font-color); - --clr-app-alert-danger-action-active-color: var( - --clr-app-alert-danger-font-color - ); - --clr-app-alert-danger-close-icon-opacity: var( - --clr-app-alert-close-icon-opacity - ); - --clr-app-alert-danger-close-icon-hover-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-danger-close-icon-hover-opacity: var( - --clr-app-alert-close-icon-hover-opacity - ); - --clr-app-alert-success-border-color: none; - --clr-app-alert-success-bg-color: var(--clr-color-success-700); - --clr-app-alert-success-icon-color: var(--clr-app-alert-close-icon-color); - --clr-app-alert-success-font-color: var(--clr-app-level-alert-color); - --clr-app-alert-success-close-icon-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-success-action-color: var( - --clr-app-alert-success-font-color - ); - --clr-app-alert-success-action-active-color: var( - --clr-app-alert-success-font-color - ); - --clr-app-alert-success-close-icon-opacity: var( - --clr-app-alert-close-icon-opacity - ); - --clr-app-alert-success-close-icon-hover-color: var( - --clr-app-alert-close-icon-color - ); - --clr-app-alert-success-close-icon-hover-opacity: var( - --clr-app-alert-close-icon-hover-opacity - ); - --clr-app-alert-pager-text-color: var(--clr-color-neutral-0, white); - --clr-app-alert-info-pager-bg-color: var(--clr-color-action-800, #00567a); - --clr-app-alert-warning-pager-bg-color: var( - --clr-color-warning-900, - #8f5a00 - ); - --clr-app-alert-danger-pager-bg-color: var(--clr-color-danger-900, #991700); + --clr-alert-action-color: var(--clr-color-neutral-700); + --clr-alert-action-active-color: var(--clr-color-secondary-action-900); + --clr-alert-close-icon-color: var(--clr-alert-action-color); + --clr-alert-close-icon-hover-color: var(--clr-alert-action-active-color); + --clr-alert-close-icon-opacity: 1; + --clr-alert-close-icon-hover-opacity: 1; + --clr-app-level-alert-color: var(--clr-color-neutral-0); + --clr-app-alert-close-icon-color: var(--clr-app-level-alert-color); + --clr-app-alert-close-icon-opacity: 0.8; + --clr-app-alert-close-icon-hover-opacity: 1; + --clr-alert-borderradius: var(--clr-global-borderradius); + --clr-alert-info-bg-color: var(--clr-color-action-50); + --clr-alert-info-font-color: var(--clr-color-neutral-700); + --clr-alert-info-border-color: var(--clr-color-action-800); + --clr-alert-info-icon-color: var(--clr-color-action-800); + --clr-alert-info-action-color: var(--clr-alert-action-color); + --clr-alert-info-action-active-color: var(--clr-alert-action-active-color); + --clr-alert-info-close-icon-color: var(--clr-alert-close-icon-color); + --clr-alert-info-close-icon-opacity: var(--clr-alert-close-icon-opacity); + --clr-alert-info-close-icon-hover-color: var( + --clr-alert-close-icon-hover-color + ); + --clr-alert-info-close-icon-hover-opacity: var( + --clr-alert-close-icon-hover-opacity + ); + --clr-alert-success-bg-color: var(--clr-color-success-50); + --clr-alert-success-font-color: var(--clr-color-neutral-700); + --clr-alert-success-border-color: var(--clr-color-success-800); + --clr-alert-success-icon-color: var(--clr-color-success-800); + --clr-alert-success-action-color: var(--clr-alert-action-color); + --clr-alert-success-action-active-color: var(--clr-alert-action-active-color); + --clr-alert-success-close-icon-color: var(--clr-alert-close-icon-color); + --clr-alert-success-close-icon-opacity: var(--clr-alert-close-icon-opacity); + --clr-alert-success-close-icon-hover-color: var( + --clr-alert-close-icon-hover-color + ); + --clr-alert-success-close-icon-hover-opacity: var( + --clr-alert-close-icon-hover-opacity + ); + --clr-alert-warning-bg-color: var(--clr-color-warning-100); + --clr-alert-warning-font-color: var(--clr-color-neutral-900); + --clr-alert-warning-border-color: var(--clr-color-warning-800); + --clr-alert-warning-icon-color: var(--clr-color-warning-800); + --clr-alert-warning-action-color: var(--clr-alert-action-color); + --clr-alert-warning-action-active-color: var(--clr-alert-action-active-color); + --clr-alert-warning-close-icon-color: var(--clr-alert-close-icon-color); + --clr-alert-warning-close-icon-opacity: var(--clr-alert-close-icon-opacity); + --clr-alert-warning-close-icon-hover-color: var( + --clr-alert-close-icon-hover-color + ); + --clr-alert-warning-close-icon-hover-opacity: var( + --clr-alert-close-icon-hover-opacity + ); + --clr-alert-danger-bg-color: var(--clr-color-danger-100); + --clr-alert-danger-font-color: var(--clr-color-neutral-700); + --clr-alert-danger-border-color: var(--clr-color-danger-900); + --clr-alert-danger-icon-color: var(--clr-color-danger-900); + --clr-alert-danger-action-color: var(--clr-alert-action-color); + --clr-alert-danger-action-active-color: var(--clr-alert-action-active-color); + --clr-alert-danger-close-icon-color: var(--clr-alert-close-icon-color); + --clr-alert-danger-close-icon-opacity: var(--clr-alert-close-icon-opacity); + --clr-alert-danger-close-icon-hover-color: var( + --clr-alert-close-icon-hover-color + ); + --clr-alert-danger-close-icon-hover-opacity: var( + --clr-alert-close-icon-hover-opacity + ); + --clr-app-alert-info-bg-color: var(--clr-color-action-600); + --clr-app-alert-info-font-color: var(--clr-app-level-alert-color); + --clr-app-alert-info-border-color: none; + --clr-app-alert-info-icon-color: var(--clr-app-alert-close-icon-color); + --clr-app-alert-info-action-color: var(--clr-app-alert-info-font-color); + --clr-app-alert-info-action-active-color: var( + --clr-app-alert-info-font-color + ); + --clr-app-alert-info-close-icon-color: var(--clr-app-alert-close-icon-color); + --clr-app-alert-info-close-icon-opacity: var( + --clr-app-alert-close-icon-opacity + ); + --clr-app-alert-info-close-icon-hover-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-info-close-icon-hover-opacity: var( + --clr-app-alert-close-icon-hover-opacity + ); + --clr-app-alert-warning-bg-color: hsl(26, 100%, 38%); + --clr-app-alert-warning-border-color: none; + --clr-app-alert-warning-icon-color: var(--clr-app-alert-close-icon-color); + --clr-app-alert-warning-font-color: var(--clr-app-level-alert-color); + --clr-app-alert-warning-close-icon-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-warning-action-color: var(--clr-app-alert-warning-font-color); + --clr-app-alert-warning-action-active-color: var( + --clr-app-alert-warning-font-color + ); + --clr-app-alert-warning-close-icon-opacity: var( + --clr-app-alert-close-icon-opacity + ); + --clr-app-alert-warning-close-icon-hover-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-warning-close-icon-hover-opacity: var( + --clr-app-alert-close-icon-hover-opacity + ); + --clr-app-alert-danger-bg-color: var(--clr-color-danger-800); + --clr-app-alert-danger-border-color: none; + --clr-app-alert-danger-icon-color: var(--clr-app-alert-close-icon-color); + --clr-app-alert-danger-font-color: var(--clr-app-level-alert-color); + --clr-app-alert-danger-close-icon-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-danger-action-color: var(--clr-app-alert-danger-font-color); + --clr-app-alert-danger-action-active-color: var( + --clr-app-alert-danger-font-color + ); + --clr-app-alert-danger-close-icon-opacity: var( + --clr-app-alert-close-icon-opacity + ); + --clr-app-alert-danger-close-icon-hover-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-danger-close-icon-hover-opacity: var( + --clr-app-alert-close-icon-hover-opacity + ); + --clr-app-alert-success-border-color: none; + --clr-app-alert-success-bg-color: var(--clr-color-success-700); + --clr-app-alert-success-icon-color: var(--clr-app-alert-close-icon-color); + --clr-app-alert-success-font-color: var(--clr-app-level-alert-color); + --clr-app-alert-success-close-icon-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-success-action-color: var(--clr-app-alert-success-font-color); + --clr-app-alert-success-action-active-color: var( + --clr-app-alert-success-font-color + ); + --clr-app-alert-success-close-icon-opacity: var( + --clr-app-alert-close-icon-opacity + ); + --clr-app-alert-success-close-icon-hover-color: var( + --clr-app-alert-close-icon-color + ); + --clr-app-alert-success-close-icon-hover-opacity: var( + --clr-app-alert-close-icon-hover-opacity + ); + --clr-app-alert-pager-text-color: var(--clr-color-neutral-0, white); + --clr-app-alert-info-pager-bg-color: var(--clr-color-action-800, #00567a); + --clr-app-alert-warning-pager-bg-color: var(--clr-color-warning-900, #8f5a00); + --clr-app-alert-danger-pager-bg-color: var(--clr-color-danger-900, #991700); } .alert-icon { - height: 1.2rem; - width: 1.2rem; - margin-left: -0.15rem; - margin-top: -0.2rem; + height: 1.2rem; + width: 1.2rem; + margin-left: -0.15rem; + margin-top: -0.2rem; } .alert-icon-wrapper { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.25rem; - flex: 0 0 1.25rem; - -ms-flex-item-align: start; - align-self: start; - padding-top: 0.05rem; - height: 0.9rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.25rem; + flex: 0 0 1.25rem; + -ms-flex-item-align: start; + align-self: start; + padding-top: 0.05rem; + height: 0.9rem; } .alert-item { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: nowrap; - flex-wrap: nowrap; - min-height: 0.9rem; - margin-bottom: 0.3rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + min-height: 0.9rem; + margin-bottom: 0.3rem; } .alert-item:last-child { - margin-bottom: 0; + margin-bottom: 0; } .alert-items { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-flow: column nowrap; - flex-flow: column nowrap; - padding: 0.4rem 0.55rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + padding: 0.4rem 0.55rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } -.alert-item > span, -.alert-text { - display: inline-block; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-negative: 1; - flex-shrink: 1; - -ms-flex-preferred-size: 98%; - flex-basis: 98%; - max-width: 98%; - margin-right: 0.6rem; - text-align: left; +.alert-item > span, +.alert-text { + display: inline-block; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -ms-flex-negative: 1; + flex-shrink: 1; + -ms-flex-preferred-size: 98%; + flex-basis: 98%; + max-width: 98%; + margin-right: 0.6rem; + text-align: left; } .alert { - font-size: 0.65rem; - letter-spacing: normal; - line-height: 0.9rem; - position: relative; - -webkit-box-sizing: border-box; - box-sizing: border-box; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - width: auto; - border-radius: 0.15rem; - border-radius: var(--clr-alert-borderradius, 0.15rem); - margin-top: 0.3rem; - background: #e3f5fc; - background: var(--clr-alert-info-bg-color, #e3f5fc); - color: #666; - color: var(--clr-alert-info-font-color, #666); - border: 0.05rem solid; - border-color: #00567a; - border-color: var(--clr-alert-info-border-color, #00567a); + font-size: 0.65rem; + letter-spacing: normal; + line-height: 0.9rem; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + width: auto; + border-radius: 0.15rem; + border-radius: var(--clr-alert-borderradius, 0.15rem); + margin-top: 0.3rem; + background: #e3f5fc; + background: var(--clr-alert-info-bg-color, #e3f5fc); + color: #666; + color: var(--clr-alert-info-font-color, #666); + border: 0.05rem solid; + border-color: #00567a; + border-color: var(--clr-alert-info-border-color, #00567a); } .alert .alert-icon { - color: #00567a; - color: var(--clr-alert-info-icon-color, #00567a); + color: #00567a; + color: var(--clr-alert-info-icon-color, #00567a); } .alert .alert-actions .dropdown .dropdown-toggle { - color: #666; - color: var(--clr-alert-info-action-color, #666); - border-color: #666; - border-color: var(--clr-alert-info-action-color, #666); + color: #666; + color: var(--clr-alert-info-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-info-action-color, #666); } .alert .alert-action, .alert .dropdown-toggle { - color: #666; - color: var(--clr-alert-info-action-color, #666); + color: #666; + color: var(--clr-alert-info-action-color, #666); } .alert .alert-action.btn, .alert .dropdown-toggle.btn { - border-color: #666; - border-color: var(--clr-alert-info-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-info-action-color, #666); } .alert .alert-action.btn:active, .alert .alert-action.btn:focus, @@ -5021,17 +4994,17 @@ button.close { .alert .dropdown-toggle.btn:active, .alert .dropdown-toggle.btn:focus, .alert .dropdown-toggle.btn:hover { - border-color: #4f0070; - border-color: var(--clr-alert-info-action-active-color, #4f0070); + border-color: #4f0070; + border-color: var(--clr-alert-info-action-active-color, #4f0070); } .alert .alert-action.btn:active, .alert .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; - box-shadow: 0 0.05rem 0 0 #4f0070 inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-alert-info-action-active-color, #4f0070) inset; - box-shadow: 0 0.05rem 0 0 var(--clr-alert-info-action-active-color, #4f0070) - inset; + -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; + box-shadow: 0 0.05rem 0 0 #4f0070 inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-alert-info-action-active-color, #4f0070) inset; + box-shadow: 0 0.05rem 0 0 var(--clr-alert-info-action-active-color, #4f0070) + inset; } .alert .alert-action:active, .alert .alert-action:focus, @@ -5039,56 +5012,56 @@ button.close { .alert .dropdown-toggle:active, .alert .dropdown-toggle:focus, .alert .dropdown-toggle:hover { - color: #4f0070; - color: var(--clr-alert-info-action-active-color, #4f0070); - color: #4f0070; + color: #4f0070; + color: var(--clr-alert-info-action-active-color, #4f0070); + color: #4f0070; } .alert .close { - color: #666; - color: var(--clr-alert-info-close-icon-color, #666); - opacity: 1; - opacity: var(--clr-alert-info-close-icon-opacity, 1); + color: #666; + color: var(--clr-alert-info-close-icon-color, #666); + opacity: 1; + opacity: var(--clr-alert-info-close-icon-opacity, 1); } .alert .close cds-icon { - fill: #666; - fill: var(--clr-alert-info-close-icon-color, #666); + fill: #666; + fill: var(--clr-alert-info-close-icon-color, #666); } .alert .close:active, .alert .close:focus, .alert .close:hover { - color: #4f0070; - color: var(--clr-alert-info-close-icon-hover-color, #4f0070); - opacity: 1; - opacity: var(--clr-alert-info-close-icon-hover-opacity, 1); + color: #4f0070; + color: var(--clr-alert-info-close-icon-hover-color, #4f0070); + opacity: 1; + opacity: var(--clr-alert-info-close-icon-hover-opacity, 1); } .alert.alert-info { - background: #e3f5fc; - background: var(--clr-alert-info-bg-color, #e3f5fc); - color: #666; - color: var(--clr-alert-info-font-color, #666); - border: 0.05rem solid; - border-color: #00567a; - border-color: var(--clr-alert-info-border-color, #00567a); + background: #e3f5fc; + background: var(--clr-alert-info-bg-color, #e3f5fc); + color: #666; + color: var(--clr-alert-info-font-color, #666); + border: 0.05rem solid; + border-color: #00567a; + border-color: var(--clr-alert-info-border-color, #00567a); } .alert.alert-info .alert-icon { - color: #00567a; - color: var(--clr-alert-info-icon-color, #00567a); + color: #00567a; + color: var(--clr-alert-info-icon-color, #00567a); } .alert.alert-info .alert-actions .dropdown .dropdown-toggle { - color: #666; - color: var(--clr-alert-info-action-color, #666); - border-color: #666; - border-color: var(--clr-alert-info-action-color, #666); + color: #666; + color: var(--clr-alert-info-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-info-action-color, #666); } .alert.alert-info .alert-action, .alert.alert-info .dropdown-toggle { - color: #666; - color: var(--clr-alert-info-action-color, #666); + color: #666; + color: var(--clr-alert-info-action-color, #666); } .alert.alert-info .alert-action.btn, .alert.alert-info .dropdown-toggle.btn { - border-color: #666; - border-color: var(--clr-alert-info-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-info-action-color, #666); } .alert.alert-info .alert-action.btn:active, .alert.alert-info .alert-action.btn:focus, @@ -5096,17 +5069,17 @@ button.close { .alert.alert-info .dropdown-toggle.btn:active, .alert.alert-info .dropdown-toggle.btn:focus, .alert.alert-info .dropdown-toggle.btn:hover { - border-color: #4f0070; - border-color: var(--clr-alert-info-action-active-color, #4f0070); + border-color: #4f0070; + border-color: var(--clr-alert-info-action-active-color, #4f0070); } .alert.alert-info .alert-action.btn:active, .alert.alert-info .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; - box-shadow: 0 0.05rem 0 0 #4f0070 inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-alert-info-action-active-color, #4f0070) inset; - box-shadow: 0 0.05rem 0 0 var(--clr-alert-info-action-active-color, #4f0070) - inset; + -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; + box-shadow: 0 0.05rem 0 0 #4f0070 inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-alert-info-action-active-color, #4f0070) inset; + box-shadow: 0 0.05rem 0 0 var(--clr-alert-info-action-active-color, #4f0070) + inset; } .alert.alert-info .alert-action:active, .alert.alert-info .alert-action:focus, @@ -5114,56 +5087,56 @@ button.close { .alert.alert-info .dropdown-toggle:active, .alert.alert-info .dropdown-toggle:focus, .alert.alert-info .dropdown-toggle:hover { - color: #4f0070; - color: var(--clr-alert-info-action-active-color, #4f0070); - color: #4f0070; + color: #4f0070; + color: var(--clr-alert-info-action-active-color, #4f0070); + color: #4f0070; } .alert.alert-info .close { - color: #666; - color: var(--clr-alert-info-close-icon-color, #666); - opacity: 1; - opacity: var(--clr-alert-info-close-icon-opacity, 1); + color: #666; + color: var(--clr-alert-info-close-icon-color, #666); + opacity: 1; + opacity: var(--clr-alert-info-close-icon-opacity, 1); } .alert.alert-info .close cds-icon { - fill: #666; - fill: var(--clr-alert-info-close-icon-color, #666); + fill: #666; + fill: var(--clr-alert-info-close-icon-color, #666); } .alert.alert-info .close:active, .alert.alert-info .close:focus, .alert.alert-info .close:hover { - color: #4f0070; - color: var(--clr-alert-info-close-icon-hover-color, #4f0070); - opacity: 1; - opacity: var(--clr-alert-info-close-icon-hover-opacity, 1); + color: #4f0070; + color: var(--clr-alert-info-close-icon-hover-color, #4f0070); + opacity: 1; + opacity: var(--clr-alert-info-close-icon-hover-opacity, 1); } .alert.alert-success { - background: #dff0d0; - background: var(--clr-alert-success-bg-color, #dff0d0); - color: #666; - color: var(--clr-alert-success-font-color, #666); - border: 0.05rem solid; - border-color: #306b00; - border-color: var(--clr-alert-success-border-color, #306b00); + background: #dff0d0; + background: var(--clr-alert-success-bg-color, #dff0d0); + color: #666; + color: var(--clr-alert-success-font-color, #666); + border: 0.05rem solid; + border-color: #306b00; + border-color: var(--clr-alert-success-border-color, #306b00); } .alert.alert-success .alert-icon { - color: #306b00; - color: var(--clr-alert-success-icon-color, #306b00); + color: #306b00; + color: var(--clr-alert-success-icon-color, #306b00); } .alert.alert-success .alert-actions .dropdown .dropdown-toggle { - color: #666; - color: var(--clr-alert-success-action-color, #666); - border-color: #666; - border-color: var(--clr-alert-success-action-color, #666); + color: #666; + color: var(--clr-alert-success-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-success-action-color, #666); } .alert.alert-success .alert-action, .alert.alert-success .dropdown-toggle { - color: #666; - color: var(--clr-alert-success-action-color, #666); + color: #666; + color: var(--clr-alert-success-action-color, #666); } .alert.alert-success .alert-action.btn, .alert.alert-success .dropdown-toggle.btn { - border-color: #666; - border-color: var(--clr-alert-success-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-success-action-color, #666); } .alert.alert-success .alert-action.btn:active, .alert.alert-success .alert-action.btn:focus, @@ -5171,17 +5144,17 @@ button.close { .alert.alert-success .dropdown-toggle.btn:active, .alert.alert-success .dropdown-toggle.btn:focus, .alert.alert-success .dropdown-toggle.btn:hover { - border-color: #4f0070; - border-color: var(--clr-alert-success-action-active-color, #4f0070); + border-color: #4f0070; + border-color: var(--clr-alert-success-action-active-color, #4f0070); } .alert.alert-success .alert-action.btn:active, .alert.alert-success .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; - box-shadow: 0 0.05rem 0 0 #4f0070 inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-alert-success-action-active-color, #4f0070) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-alert-success-action-active-color, #4f0070) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; + box-shadow: 0 0.05rem 0 0 #4f0070 inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-alert-success-action-active-color, #4f0070) inset; + box-shadow: 0 0.05rem 0 0 + var(--clr-alert-success-action-active-color, #4f0070) inset; } .alert.alert-success .alert-action:active, .alert.alert-success .alert-action:focus, @@ -5189,56 +5162,56 @@ button.close { .alert.alert-success .dropdown-toggle:active, .alert.alert-success .dropdown-toggle:focus, .alert.alert-success .dropdown-toggle:hover { - color: #4f0070; - color: var(--clr-alert-success-action-active-color, #4f0070); - color: #4f0070; + color: #4f0070; + color: var(--clr-alert-success-action-active-color, #4f0070); + color: #4f0070; } .alert.alert-success .close { - color: #666; - color: var(--clr-alert-success-close-icon-color, #666); - opacity: 1; - opacity: var(--clr-alert-success-close-icon-opacity, 1); + color: #666; + color: var(--clr-alert-success-close-icon-color, #666); + opacity: 1; + opacity: var(--clr-alert-success-close-icon-opacity, 1); } .alert.alert-success .close cds-icon { - fill: #666; - fill: var(--clr-alert-success-close-icon-color, #666); + fill: #666; + fill: var(--clr-alert-success-close-icon-color, #666); } .alert.alert-success .close:active, .alert.alert-success .close:focus, .alert.alert-success .close:hover { - color: #4f0070; - color: var(--clr-alert-success-close-icon-hover-color, #4f0070); - opacity: 1; - opacity: var(--clr-alert-success-close-icon-hover-opacity, 1); + color: #4f0070; + color: var(--clr-alert-success-close-icon-hover-color, #4f0070); + opacity: 1; + opacity: var(--clr-alert-success-close-icon-hover-opacity, 1); } .alert.alert-warning { - background: #fff4c7; - background: var(--clr-alert-warning-bg-color, #fff4c7); - color: #333; - color: var(--clr-alert-warning-font-color, #333); - border: 0.05rem solid; - border-color: #ad7600; - border-color: var(--clr-alert-warning-border-color, #ad7600); + background: #fff4c7; + background: var(--clr-alert-warning-bg-color, #fff4c7); + color: #333; + color: var(--clr-alert-warning-font-color, #333); + border: 0.05rem solid; + border-color: #ad7600; + border-color: var(--clr-alert-warning-border-color, #ad7600); } .alert.alert-warning .alert-icon { - color: #454545; - color: var(--clr-alert-warning-icon-color, #454545); + color: #454545; + color: var(--clr-alert-warning-icon-color, #454545); } .alert.alert-warning .alert-actions .dropdown .dropdown-toggle { - color: #666; - color: var(--clr-alert-warning-action-color, #666); - border-color: #666; - border-color: var(--clr-alert-warning-action-color, #666); + color: #666; + color: var(--clr-alert-warning-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-warning-action-color, #666); } .alert.alert-warning .alert-action, .alert.alert-warning .dropdown-toggle { - color: #666; - color: var(--clr-alert-warning-action-color, #666); + color: #666; + color: var(--clr-alert-warning-action-color, #666); } .alert.alert-warning .alert-action.btn, .alert.alert-warning .dropdown-toggle.btn { - border-color: #666; - border-color: var(--clr-alert-warning-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-warning-action-color, #666); } .alert.alert-warning .alert-action.btn:active, .alert.alert-warning .alert-action.btn:focus, @@ -5246,17 +5219,17 @@ button.close { .alert.alert-warning .dropdown-toggle.btn:active, .alert.alert-warning .dropdown-toggle.btn:focus, .alert.alert-warning .dropdown-toggle.btn:hover { - border-color: #4f0070; - border-color: var(--clr-alert-warning-action-active-color, #4f0070); + border-color: #4f0070; + border-color: var(--clr-alert-warning-action-active-color, #4f0070); } .alert.alert-warning .alert-action.btn:active, .alert.alert-warning .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; - box-shadow: 0 0.05rem 0 0 #4f0070 inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-alert-warning-action-active-color, #4f0070) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-alert-warning-action-active-color, #4f0070) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; + box-shadow: 0 0.05rem 0 0 #4f0070 inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-alert-warning-action-active-color, #4f0070) inset; + box-shadow: 0 0.05rem 0 0 + var(--clr-alert-warning-action-active-color, #4f0070) inset; } .alert.alert-warning .alert-action:active, .alert.alert-warning .alert-action:focus, @@ -5264,56 +5237,56 @@ button.close { .alert.alert-warning .dropdown-toggle:active, .alert.alert-warning .dropdown-toggle:focus, .alert.alert-warning .dropdown-toggle:hover { - color: #4f0070; - color: var(--clr-alert-warning-action-active-color, #4f0070); - color: #4f0070; + color: #4f0070; + color: var(--clr-alert-warning-action-active-color, #4f0070); + color: #4f0070; } .alert.alert-warning .close { - color: #666; - color: var(--clr-alert-warning-close-icon-color, #666); - opacity: 1; - opacity: var(--clr-alert-warning-close-icon-opacity, 1); + color: #666; + color: var(--clr-alert-warning-close-icon-color, #666); + opacity: 1; + opacity: var(--clr-alert-warning-close-icon-opacity, 1); } .alert.alert-warning .close cds-icon { - fill: #666; - fill: var(--clr-alert-warning-close-icon-color, #666); + fill: #666; + fill: var(--clr-alert-warning-close-icon-color, #666); } .alert.alert-warning .close:active, .alert.alert-warning .close:focus, .alert.alert-warning .close:hover { - color: #4f0070; - color: var(--clr-alert-warning-close-icon-hover-color, #4f0070); - opacity: 1; - opacity: var(--clr-alert-warning-close-icon-hover-opacity, 1); + color: #4f0070; + color: var(--clr-alert-warning-close-icon-hover-color, #4f0070); + opacity: 1; + opacity: var(--clr-alert-warning-close-icon-hover-opacity, 1); } .alert.alert-danger { - background: #feddd7; - background: var(--clr-alert-danger-bg-color, #feddd7); - color: #666; - color: var(--clr-alert-danger-font-color, #666); - border: 0.05rem solid; - border-color: #991700; - border-color: var(--clr-alert-danger-border-color, #991700); + background: #feddd7; + background: var(--clr-alert-danger-bg-color, #feddd7); + color: #666; + color: var(--clr-alert-danger-font-color, #666); + border: 0.05rem solid; + border-color: #991700; + border-color: var(--clr-alert-danger-border-color, #991700); } .alert.alert-danger .alert-icon { - color: #991700; - color: var(--clr-alert-danger-icon-color, #991700); + color: #991700; + color: var(--clr-alert-danger-icon-color, #991700); } .alert.alert-danger .alert-actions .dropdown .dropdown-toggle { - color: #666; - color: var(--clr-alert-danger-action-color, #666); - border-color: #666; - border-color: var(--clr-alert-danger-action-color, #666); + color: #666; + color: var(--clr-alert-danger-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-danger-action-color, #666); } .alert.alert-danger .alert-action, .alert.alert-danger .dropdown-toggle { - color: #666; - color: var(--clr-alert-danger-action-color, #666); + color: #666; + color: var(--clr-alert-danger-action-color, #666); } .alert.alert-danger .alert-action.btn, .alert.alert-danger .dropdown-toggle.btn { - border-color: #666; - border-color: var(--clr-alert-danger-action-color, #666); + border-color: #666; + border-color: var(--clr-alert-danger-action-color, #666); } .alert.alert-danger .alert-action.btn:active, .alert.alert-danger .alert-action.btn:focus, @@ -5321,17 +5294,17 @@ button.close { .alert.alert-danger .dropdown-toggle.btn:active, .alert.alert-danger .dropdown-toggle.btn:focus, .alert.alert-danger .dropdown-toggle.btn:hover { - border-color: #4f0070; - border-color: var(--clr-alert-danger-action-active-color, #4f0070); + border-color: #4f0070; + border-color: var(--clr-alert-danger-action-active-color, #4f0070); } .alert.alert-danger .alert-action.btn:active, .alert.alert-danger .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; - box-shadow: 0 0.05rem 0 0 #4f0070 inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-alert-danger-action-active-color, #4f0070) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-alert-danger-action-active-color, #4f0070) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #4f0070 inset; + box-shadow: 0 0.05rem 0 0 #4f0070 inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-alert-danger-action-active-color, #4f0070) inset; + box-shadow: 0 0.05rem 0 0 var(--clr-alert-danger-action-active-color, #4f0070) + inset; } .alert.alert-danger .alert-action:active, .alert.alert-danger .alert-action:focus, @@ -5339,126 +5312,126 @@ button.close { .alert.alert-danger .dropdown-toggle:active, .alert.alert-danger .dropdown-toggle:focus, .alert.alert-danger .dropdown-toggle:hover { - color: #4f0070; - color: var(--clr-alert-danger-action-active-color, #4f0070); - color: #4f0070; + color: #4f0070; + color: var(--clr-alert-danger-action-active-color, #4f0070); + color: #4f0070; } .alert.alert-danger .close { - color: #666; - color: var(--clr-alert-danger-close-icon-color, #666); - opacity: 1; - opacity: var(--clr-alert-danger-close-icon-opacity, 1); + color: #666; + color: var(--clr-alert-danger-close-icon-color, #666); + opacity: 1; + opacity: var(--clr-alert-danger-close-icon-opacity, 1); } .alert.alert-danger .close cds-icon { - fill: #666; - fill: var(--clr-alert-danger-close-icon-color, #666); + fill: #666; + fill: var(--clr-alert-danger-close-icon-color, #666); } .alert.alert-danger .close:active, .alert.alert-danger .close:focus, .alert.alert-danger .close:hover { - color: #4f0070; - color: var(--clr-alert-danger-close-icon-hover-color, #4f0070); - opacity: 1; - opacity: var(--clr-alert-danger-close-icon-hover-opacity, 1); + color: #4f0070; + color: var(--clr-alert-danger-close-icon-hover-color, #4f0070); + opacity: 1; + opacity: var(--clr-alert-danger-close-icon-hover-opacity, 1); } .alert .alert-item .clr-icon { - height: 0.9rem; - width: 0.9rem; - margin-right: 0.3rem; + height: 0.9rem; + width: 0.9rem; + margin-right: 0.3rem; } .alert .alert-item .clr-icon + .alert-text { - padding-left: 0; + padding-left: 0; } .alert .alert-item .clr-icon + .alert-text::before { - content: none; + content: none; } .alert .alert-actions { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - white-space: nowrap; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + white-space: nowrap; } .alert .alert-actions .dropdown:last-child { - margin-right: -0.1rem; + margin-right: -0.1rem; } .alert .alert-actions .dropdown-item { - color: #666; - color: var(--clr-dropdown-text-color, #666); - font-size: 0.7rem; - line-height: 1.2rem; - letter-spacing: normal; + color: #666; + color: var(--clr-dropdown-text-color, #666); + font-size: 0.7rem; + line-height: 1.2rem; + letter-spacing: normal; } .alert .alert-action:not(:last-child) { - margin-right: 0.6rem; + margin-right: 0.6rem; } .alert .alert-action, .alert .dropdown-toggle { - text-decoration: underline; + text-decoration: underline; } .alert .alert-action button.dropdown-toggle:not(.btn) { - background: 0 0; - cursor: pointer; - color: #666; - color: var(--clr-dropdown-text-color, #666); + background: 0 0; + cursor: pointer; + color: #666; + color: var(--clr-dropdown-text-color, #666); } .alert .dropdown-toggle:not(.btn) { - display: inline-block; - background: 0 0; - border: none; + display: inline-block; + background: 0 0; + border: none; } .alert .close { - width: 1.2rem; - display: block; - height: 1.8rem; - -webkit-box-flex: 0; - -ms-flex: 0 0 1.4rem; - flex: 0 0 1.4rem; - -webkit-box-ordinal-group: 101; - -ms-flex-order: 100; - order: 100; - padding-right: 0.2rem; + width: 1.2rem; + display: block; + height: 1.8rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.4rem; + flex: 0 0 1.4rem; + -webkit-box-ordinal-group: 101; + -ms-flex-order: 100; + order: 100; + padding-right: 0.2rem; } .alert .close cds-icon { - margin-top: -0.15rem; - height: 1.15rem; - width: 1.15rem; + margin-top: -0.15rem; + height: 1.15rem; + width: 1.15rem; } .alert .close ~ .alert-item > .alert-actions { - padding-right: 0.6rem; + padding-right: 0.6rem; } .alert .close ~ .alert-item > .alert-actions > .alert-action:last-child { - margin-right: 0.6rem; + margin-right: 0.6rem; } .alert-app-level { - margin: 0; - border: none; - border-radius: 0; - overflow-y: auto; - background: #0072a3; - background: var(--clr-app-alert-info-bg-color, #0072a3); - color: #fff; - color: var(--clr-app-alert-info-font-color, #fff); - border: none; + margin: 0; + border: none; + border-radius: 0; + overflow-y: auto; + background: #0072a3; + background: var(--clr-app-alert-info-bg-color, #0072a3); + color: #fff; + color: var(--clr-app-alert-info-font-color, #fff); + border: none; } .alert-app-level .alert-icon { - color: #fff; - color: var(--clr-app-alert-info-icon-color, #fff); + color: #fff; + color: var(--clr-app-alert-info-icon-color, #fff); } .alert-app-level .alert-actions .dropdown .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-info-action-color, #fff); - border-color: #fff; - border-color: var(--clr-app-alert-info-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-info-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-info-action-color, #fff); } .alert-app-level .alert-action, .alert-app-level .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-info-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-info-action-color, #fff); } .alert-app-level .alert-action.btn, .alert-app-level .dropdown-toggle.btn { - border-color: #fff; - border-color: var(--clr-app-alert-info-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-info-action-color, #fff); } .alert-app-level .alert-action.btn:active, .alert-app-level .alert-action.btn:focus, @@ -5466,17 +5439,17 @@ button.close { .alert-app-level .dropdown-toggle.btn:active, .alert-app-level .dropdown-toggle.btn:focus, .alert-app-level .dropdown-toggle.btn:hover { - border-color: #fff; - border-color: var(--clr-app-alert-info-action-active-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-info-action-active-color, #fff); } .alert-app-level .alert-action.btn:active, .alert-app-level .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; - box-shadow: 0 0.05rem 0 0 #fff inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-info-action-active-color, #fff) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-info-action-active-color, #fff) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; + box-shadow: 0 0.05rem 0 0 #fff inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-info-action-active-color, #fff) inset; + box-shadow: 0 0.05rem 0 0 var(--clr-app-alert-info-action-active-color, #fff) + inset; } .alert-app-level .alert-action:active, .alert-app-level .alert-action:focus, @@ -5484,54 +5457,54 @@ button.close { .alert-app-level .dropdown-toggle:active, .alert-app-level .dropdown-toggle:focus, .alert-app-level .dropdown-toggle:hover { - color: #fff; - color: var(--clr-app-alert-info-action-active-color, #fff); - color: #fff; + color: #fff; + color: var(--clr-app-alert-info-action-active-color, #fff); + color: #fff; } .alert-app-level .close { - color: #fff; - color: var(--clr-app-alert-info-close-icon-color, #fff); - opacity: 0.8; - opacity: var(--clr-app-alert-info-close-icon-opacity, 0.8); + color: #fff; + color: var(--clr-app-alert-info-close-icon-color, #fff); + opacity: 0.8; + opacity: var(--clr-app-alert-info-close-icon-opacity, 0.8); } .alert-app-level .close cds-icon { - fill: #fff; - fill: var(--clr-app-alert-info-close-icon-color, #fff); + fill: #fff; + fill: var(--clr-app-alert-info-close-icon-color, #fff); } .alert-app-level .close:active, .alert-app-level .close:focus, .alert-app-level .close:hover { - color: #fff; - color: var(--clr-app-alert-info-close-icon-hover-color, #fff); - opacity: 1; - opacity: var(--clr-app-alert-info-close-icon-hover-opacity, 1); + color: #fff; + color: var(--clr-app-alert-info-close-icon-hover-color, #fff); + opacity: 1; + opacity: var(--clr-app-alert-info-close-icon-hover-opacity, 1); } .alert-app-level.alert-info { - background: #0072a3; - background: var(--clr-app-alert-info-bg-color, #0072a3); - color: #fff; - color: var(--clr-app-alert-info-font-color, #fff); - border: none; + background: #0072a3; + background: var(--clr-app-alert-info-bg-color, #0072a3); + color: #fff; + color: var(--clr-app-alert-info-font-color, #fff); + border: none; } .alert-app-level.alert-info .alert-icon { - color: #fff; - color: var(--clr-app-alert-info-icon-color, #fff); + color: #fff; + color: var(--clr-app-alert-info-icon-color, #fff); } .alert-app-level.alert-info .alert-actions .dropdown .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-info-action-color, #fff); - border-color: #fff; - border-color: var(--clr-app-alert-info-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-info-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-info-action-color, #fff); } .alert-app-level.alert-info .alert-action, .alert-app-level.alert-info .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-info-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-info-action-color, #fff); } .alert-app-level.alert-info .alert-action.btn, .alert-app-level.alert-info .dropdown-toggle.btn { - border-color: #fff; - border-color: var(--clr-app-alert-info-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-info-action-color, #fff); } .alert-app-level.alert-info .alert-action.btn:active, .alert-app-level.alert-info .alert-action.btn:focus, @@ -5539,17 +5512,17 @@ button.close { .alert-app-level.alert-info .dropdown-toggle.btn:active, .alert-app-level.alert-info .dropdown-toggle.btn:focus, .alert-app-level.alert-info .dropdown-toggle.btn:hover { - border-color: #fff; - border-color: var(--clr-app-alert-info-action-active-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-info-action-active-color, #fff); } .alert-app-level.alert-info .alert-action.btn:active, .alert-app-level.alert-info .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; - box-shadow: 0 0.05rem 0 0 #fff inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-info-action-active-color, #fff) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-info-action-active-color, #fff) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; + box-shadow: 0 0.05rem 0 0 #fff inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-info-action-active-color, #fff) inset; + box-shadow: 0 0.05rem 0 0 var(--clr-app-alert-info-action-active-color, #fff) + inset; } .alert-app-level.alert-info .alert-action:active, .alert-app-level.alert-info .alert-action:focus, @@ -5557,54 +5530,54 @@ button.close { .alert-app-level.alert-info .dropdown-toggle:active, .alert-app-level.alert-info .dropdown-toggle:focus, .alert-app-level.alert-info .dropdown-toggle:hover { - color: #fff; - color: var(--clr-app-alert-info-action-active-color, #fff); - color: #fff; + color: #fff; + color: var(--clr-app-alert-info-action-active-color, #fff); + color: #fff; } .alert-app-level.alert-info .close { - color: #fff; - color: var(--clr-app-alert-info-close-icon-color, #fff); - opacity: 0.8; - opacity: var(--clr-app-alert-info-close-icon-opacity, 0.8); + color: #fff; + color: var(--clr-app-alert-info-close-icon-color, #fff); + opacity: 0.8; + opacity: var(--clr-app-alert-info-close-icon-opacity, 0.8); } .alert-app-level.alert-info .close cds-icon { - fill: #fff; - fill: var(--clr-app-alert-info-close-icon-color, #fff); + fill: #fff; + fill: var(--clr-app-alert-info-close-icon-color, #fff); } .alert-app-level.alert-info .close:active, .alert-app-level.alert-info .close:focus, .alert-app-level.alert-info .close:hover { - color: #fff; - color: var(--clr-app-alert-info-close-icon-hover-color, #fff); - opacity: 1; - opacity: var(--clr-app-alert-info-close-icon-hover-opacity, 1); + color: #fff; + color: var(--clr-app-alert-info-close-icon-hover-color, #fff); + opacity: 1; + opacity: var(--clr-app-alert-info-close-icon-hover-opacity, 1); } .alert-app-level.alert-danger { - background: #c21d00; - background: var(--clr-app-alert-danger-bg-color, #c21d00); - color: #fff; - color: var(--clr-app-alert-danger-font-color, #fff); - border: none; + background: #c21d00; + background: var(--clr-app-alert-danger-bg-color, #c21d00); + color: #fff; + color: var(--clr-app-alert-danger-font-color, #fff); + border: none; } .alert-app-level.alert-danger .alert-icon { - color: #fff; - color: var(--clr-app-alert-danger-icon-color, #fff); + color: #fff; + color: var(--clr-app-alert-danger-icon-color, #fff); } .alert-app-level.alert-danger .alert-actions .dropdown .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-danger-action-color, #fff); - border-color: #fff; - border-color: var(--clr-app-alert-danger-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-danger-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-danger-action-color, #fff); } .alert-app-level.alert-danger .alert-action, .alert-app-level.alert-danger .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-danger-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-danger-action-color, #fff); } .alert-app-level.alert-danger .alert-action.btn, .alert-app-level.alert-danger .dropdown-toggle.btn { - border-color: #fff; - border-color: var(--clr-app-alert-danger-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-danger-action-color, #fff); } .alert-app-level.alert-danger .alert-action.btn:active, .alert-app-level.alert-danger .alert-action.btn:focus, @@ -5612,17 +5585,17 @@ button.close { .alert-app-level.alert-danger .dropdown-toggle.btn:active, .alert-app-level.alert-danger .dropdown-toggle.btn:focus, .alert-app-level.alert-danger .dropdown-toggle.btn:hover { - border-color: #fff; - border-color: var(--clr-app-alert-danger-action-active-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-danger-action-active-color, #fff); } .alert-app-level.alert-danger .alert-action.btn:active, .alert-app-level.alert-danger .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; - box-shadow: 0 0.05rem 0 0 #fff inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-danger-action-active-color, #fff) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-danger-action-active-color, #fff) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; + box-shadow: 0 0.05rem 0 0 #fff inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-danger-action-active-color, #fff) inset; + box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-danger-action-active-color, #fff) inset; } .alert-app-level.alert-danger .alert-action:active, .alert-app-level.alert-danger .alert-action:focus, @@ -5630,54 +5603,54 @@ button.close { .alert-app-level.alert-danger .dropdown-toggle:active, .alert-app-level.alert-danger .dropdown-toggle:focus, .alert-app-level.alert-danger .dropdown-toggle:hover { - color: #fff; - color: var(--clr-app-alert-danger-action-active-color, #fff); - color: #fff; + color: #fff; + color: var(--clr-app-alert-danger-action-active-color, #fff); + color: #fff; } .alert-app-level.alert-danger .close { - color: #fff; - color: var(--clr-app-alert-danger-close-icon-color, #fff); - opacity: 0.8; - opacity: var(--clr-app-alert-danger-close-icon-opacity, 0.8); + color: #fff; + color: var(--clr-app-alert-danger-close-icon-color, #fff); + opacity: 0.8; + opacity: var(--clr-app-alert-danger-close-icon-opacity, 0.8); } .alert-app-level.alert-danger .close cds-icon { - fill: #fff; - fill: var(--clr-app-alert-danger-close-icon-color, #fff); + fill: #fff; + fill: var(--clr-app-alert-danger-close-icon-color, #fff); } .alert-app-level.alert-danger .close:active, .alert-app-level.alert-danger .close:focus, .alert-app-level.alert-danger .close:hover { - color: #fff; - color: var(--clr-app-alert-danger-close-icon-hover-color, #fff); - opacity: 1; - opacity: var(--clr-app-alert-danger-close-icon-hover-opacity, 1); + color: #fff; + color: var(--clr-app-alert-danger-close-icon-hover-color, #fff); + opacity: 1; + opacity: var(--clr-app-alert-danger-close-icon-hover-opacity, 1); } .alert-app-level.alert-warning { - background: #c25400; - background: var(--clr-app-alert-warning-bg-color, #c25400); - color: #fff; - color: var(--clr-app-alert-warning-font-color, #fff); - border: none; + background: #c25400; + background: var(--clr-app-alert-warning-bg-color, #c25400); + color: #fff; + color: var(--clr-app-alert-warning-font-color, #fff); + border: none; } .alert-app-level.alert-warning .alert-icon { - color: #fff; - color: var(--clr-app-alert-warning-icon-color, #fff); + color: #fff; + color: var(--clr-app-alert-warning-icon-color, #fff); } .alert-app-level.alert-warning .alert-actions .dropdown .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-warning-action-color, #fff); - border-color: #fff; - border-color: var(--clr-app-alert-warning-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-warning-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-warning-action-color, #fff); } .alert-app-level.alert-warning .alert-action, .alert-app-level.alert-warning .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-warning-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-warning-action-color, #fff); } .alert-app-level.alert-warning .alert-action.btn, .alert-app-level.alert-warning .dropdown-toggle.btn { - border-color: #fff; - border-color: var(--clr-app-alert-warning-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-warning-action-color, #fff); } .alert-app-level.alert-warning .alert-action.btn:active, .alert-app-level.alert-warning .alert-action.btn:focus, @@ -5685,17 +5658,17 @@ button.close { .alert-app-level.alert-warning .dropdown-toggle.btn:active, .alert-app-level.alert-warning .dropdown-toggle.btn:focus, .alert-app-level.alert-warning .dropdown-toggle.btn:hover { - border-color: #fff; - border-color: var(--clr-app-alert-warning-action-active-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-warning-action-active-color, #fff); } .alert-app-level.alert-warning .alert-action.btn:active, .alert-app-level.alert-warning .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; - box-shadow: 0 0.05rem 0 0 #fff inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-warning-action-active-color, #fff) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-warning-action-active-color, #fff) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; + box-shadow: 0 0.05rem 0 0 #fff inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-warning-action-active-color, #fff) inset; + box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-warning-action-active-color, #fff) inset; } .alert-app-level.alert-warning .alert-action:active, .alert-app-level.alert-warning .alert-action:focus, @@ -5703,54 +5676,54 @@ button.close { .alert-app-level.alert-warning .dropdown-toggle:active, .alert-app-level.alert-warning .dropdown-toggle:focus, .alert-app-level.alert-warning .dropdown-toggle:hover { - color: #fff; - color: var(--clr-app-alert-warning-action-active-color, #fff); - color: #fff; + color: #fff; + color: var(--clr-app-alert-warning-action-active-color, #fff); + color: #fff; } .alert-app-level.alert-warning .close { - color: #fff; - color: var(--clr-app-alert-warning-close-icon-color, #fff); - opacity: 0.8; - opacity: var(--clr-app-alert-warning-close-icon-opacity, 0.8); + color: #fff; + color: var(--clr-app-alert-warning-close-icon-color, #fff); + opacity: 0.8; + opacity: var(--clr-app-alert-warning-close-icon-opacity, 0.8); } .alert-app-level.alert-warning .close cds-icon { - fill: #fff; - fill: var(--clr-app-alert-warning-close-icon-color, #fff); + fill: #fff; + fill: var(--clr-app-alert-warning-close-icon-color, #fff); } .alert-app-level.alert-warning .close:active, .alert-app-level.alert-warning .close:focus, .alert-app-level.alert-warning .close:hover { - color: #fff; - color: var(--clr-app-alert-warning-close-icon-hover-color, #fff); - opacity: 1; - opacity: var(--clr-app-alert-warning-close-icon-hover-opacity, 1); + color: #fff; + color: var(--clr-app-alert-warning-close-icon-hover-color, #fff); + opacity: 1; + opacity: var(--clr-app-alert-warning-close-icon-hover-opacity, 1); } .alert-app-level.alert-success { - background: #3c8500; - background: var(--clr-app-alert-success-bg-color, #3c8500); - color: #fff; - color: var(--clr-app-alert-success-font-color, #fff); - border: none; + background: #3c8500; + background: var(--clr-app-alert-success-bg-color, #3c8500); + color: #fff; + color: var(--clr-app-alert-success-font-color, #fff); + border: none; } .alert-app-level.alert-success .alert-icon { - color: #fff; - color: var(--clr-app-alert-success-icon-color, #fff); + color: #fff; + color: var(--clr-app-alert-success-icon-color, #fff); } .alert-app-level.alert-success .alert-actions .dropdown .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-success-action-color, #fff); - border-color: #fff; - border-color: var(--clr-app-alert-success-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-success-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-success-action-color, #fff); } .alert-app-level.alert-success .alert-action, .alert-app-level.alert-success .dropdown-toggle { - color: #fff; - color: var(--clr-app-alert-success-action-color, #fff); + color: #fff; + color: var(--clr-app-alert-success-action-color, #fff); } .alert-app-level.alert-success .alert-action.btn, .alert-app-level.alert-success .dropdown-toggle.btn { - border-color: #fff; - border-color: var(--clr-app-alert-success-action-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-success-action-color, #fff); } .alert-app-level.alert-success .alert-action.btn:active, .alert-app-level.alert-success .alert-action.btn:focus, @@ -5758,17 +5731,17 @@ button.close { .alert-app-level.alert-success .dropdown-toggle.btn:active, .alert-app-level.alert-success .dropdown-toggle.btn:focus, .alert-app-level.alert-success .dropdown-toggle.btn:hover { - border-color: #fff; - border-color: var(--clr-app-alert-success-action-active-color, #fff); + border-color: #fff; + border-color: var(--clr-app-alert-success-action-active-color, #fff); } .alert-app-level.alert-success .alert-action.btn:active, .alert-app-level.alert-success .dropdown-toggle.btn:active { - -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; - box-shadow: 0 0.05rem 0 0 #fff inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-success-action-active-color, #fff) inset; - box-shadow: 0 0.05rem 0 0 - var(--clr-app-alert-success-action-active-color, #fff) inset; + -webkit-box-shadow: 0 0.05rem 0 0 #fff inset; + box-shadow: 0 0.05rem 0 0 #fff inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-success-action-active-color, #fff) inset; + box-shadow: 0 0.05rem 0 0 + var(--clr-app-alert-success-action-active-color, #fff) inset; } .alert-app-level.alert-success .alert-action:active, .alert-app-level.alert-success .alert-action:focus, @@ -5776,263 +5749,261 @@ button.close { .alert-app-level.alert-success .dropdown-toggle:active, .alert-app-level.alert-success .dropdown-toggle:focus, .alert-app-level.alert-success .dropdown-toggle:hover { - color: #fff; - color: var(--clr-app-alert-success-action-active-color, #fff); - color: #fff; + color: #fff; + color: var(--clr-app-alert-success-action-active-color, #fff); + color: #fff; } .alert-app-level.alert-success .close { - color: #fff; - color: var(--clr-app-alert-success-close-icon-color, #fff); - opacity: 0.8; - opacity: var(--clr-app-alert-success-close-icon-opacity, 0.8); + color: #fff; + color: var(--clr-app-alert-success-close-icon-color, #fff); + opacity: 0.8; + opacity: var(--clr-app-alert-success-close-icon-opacity, 0.8); } .alert-app-level.alert-success .close cds-icon { - fill: #fff; - fill: var(--clr-app-alert-success-close-icon-color, #fff); + fill: #fff; + fill: var(--clr-app-alert-success-close-icon-color, #fff); } .alert-app-level.alert-success .close:active, .alert-app-level.alert-success .close:focus, .alert-app-level.alert-success .close:hover { - color: #fff; - color: var(--clr-app-alert-success-close-icon-hover-color, #fff); - opacity: 1; - opacity: var(--clr-app-alert-success-close-icon-hover-opacity, 1); + color: #fff; + color: var(--clr-app-alert-success-close-icon-hover-color, #fff); + opacity: 1; + opacity: var(--clr-app-alert-success-close-icon-hover-opacity, 1); } .alert-app-level .alert-items { - padding-top: 0.3rem; - padding-bottom: 0.3rem; + padding-top: 0.3rem; + padding-bottom: 0.3rem; } .alert-app-level .alert-item { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - min-height: 1.2rem; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + min-height: 1.2rem; } .alert-app-level .alert-item .btn { - margin: 0; + margin: 0; } .alert-app-level .alert-item > span, .alert-app-level .alert-text { - -webkit-box-flex: 0; - -ms-flex: 0 1 100%; - flex: 0 1 100%; + -webkit-box-flex: 0; + -ms-flex: 0 1 100%; + flex: 0 1 100%; } .alert-app-level .alert-icon-wrapper { - margin-top: 0.15rem; + margin-top: 0.15rem; } .alert-app-level .close { - height: 1.8rem; - overflow: hidden; + height: 1.8rem; + overflow: hidden; } .alert-app-level .close cds-icon { - margin-top: -0.25rem; + margin-top: -0.25rem; } .alert-app-level .alert-action, .alert-app-level .dropdown-toggle { - text-decoration: none; + text-decoration: none; } .alert-sm { - font-size: 0.55rem; - letter-spacing: normal; - line-height: 0.8rem; + font-size: 0.55rem; + letter-spacing: normal; + line-height: 0.8rem; } .alert-sm .alert-items { - padding: 0.15rem 0.25rem; + padding: 0.15rem 0.25rem; } .alert-sm .alert-item { - padding-top: 0.05rem; - margin-bottom: 0.2rem; + padding-top: 0.05rem; + margin-bottom: 0.2rem; } .alert-sm .alert-item:last-child { - margin-bottom: 0; + margin-bottom: 0; } .alert-sm .alert-icon-wrapper { - padding-top: 0; - height: 0.8rem; + padding-top: 0; + height: 0.8rem; } .alert-sm .alert-icon { - margin-left: -0.2rem; - margin-top: -0.2rem; + margin-left: -0.2rem; + margin-top: -0.2rem; } .alert-sm .alert-item > span, .alert-sm .alert-text { - margin-right: 0.3rem; + margin-right: 0.3rem; } .alert-sm .close { - padding-right: 0; - -webkit-box-flex: 0; - -ms-flex: 0 0 1.2rem; - flex: 0 0 1.2rem; - height: 1.2rem; - line-height: 1.2rem; + padding-right: 0; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.2rem; + flex: 0 0 1.2rem; + height: 1.2rem; + line-height: 1.2rem; } .alert-sm .close cds-icon { - margin-top: -0.25rem; - margin-right: -0.05rem; - height: 1rem; - width: 1rem; - line-height: 1.05rem; + margin-top: -0.25rem; + margin-right: -0.05rem; + height: 1rem; + width: 1rem; + line-height: 1.05rem; } @media screen and (max-width: 768px) { - .alert .alert-item { - -ms-flex-wrap: wrap; - flex-wrap: wrap; - } - .alert .alert-text { - margin-right: 0; - max-width: 90%; - width: 90%; - -ms-flex-preferred-size: 90%; - flex-basis: 90%; - } - .alert .alert-actions { - -webkit-box-flex: 1; - -ms-flex: 1 0 100%; - flex: 1 0 100%; - padding-top: 0.15rem; - padding-left: 1.2rem; - } - .alerts-pager { - margin-top: 0.15rem; - } - .alert-app-level .alert-actions { - margin-left: 2.25rem; - } + .alert .alert-item { + -ms-flex-wrap: wrap; + flex-wrap: wrap; + } + .alert .alert-text { + margin-right: 0; + max-width: 90%; + width: 90%; + -ms-flex-preferred-size: 90%; + flex-basis: 90%; + } + .alert .alert-actions { + -webkit-box-flex: 1; + -ms-flex: 1 0 100%; + flex: 1 0 100%; + padding-top: 0.15rem; + padding-left: 1.2rem; + } + .alerts-pager { + margin-top: 0.15rem; + } + .alert-app-level .alert-actions { + margin-left: 2.25rem; + } } .alert-hidden { - display: none; + display: none; } .card .alert { - margin: 0.3rem 0; + margin: 0.3rem 0; } .modal .alert + .modal-header, .modal .alert + .modal-header--accessible { - margin-top: 0.6rem; + margin-top: 0.6rem; } .alerts.alert-info { - background: #00567a; - background: var(--clr-app-alert-info-pager-bg-color, #00567a); + background: #00567a; + background: var(--clr-app-alert-info-pager-bg-color, #00567a); } .alerts.alert-danger { - background: #991700; - background: var(--clr-app-alert-danger-pager-bg-color, #991700); + background: #991700; + background: var(--clr-app-alert-danger-pager-bg-color, #991700); } .alerts.alert-warning { - background: #8f5a00; - background: var(--clr-app-alert-warning-pager-bg-color, #8f5a00); + background: #8f5a00; + background: var(--clr-app-alert-warning-pager-bg-color, #8f5a00); } .alerts.alert-success { - background: #255200; - background: var(--clr-color-success-900, #255200); + background: #255200; + background: var(--clr-color-success-900, #255200); } .alerts-pager { - color: #fff; - color: var(--clr-app-alert-pager-text-color, #fff); - font-size: 0.65rem; - letter-spacing: normal; - float: left; - min-height: 1.8rem; - text-align: center; - width: 7.2rem; + color: #fff; + color: var(--clr-app-alert-pager-text-color, #fff); + font-size: 0.65rem; + letter-spacing: normal; + float: left; + min-height: 1.8rem; + text-align: center; + width: 7.2rem; } .alerts-pager-button { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - color: #fff; - color: var(--clr-app-alert-pager-text-color, #fff); - cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + color: #fff; + color: var(--clr-app-alert-pager-text-color, #fff); + cursor: pointer; } button.alerts-pager-button { - cursor: pointer; + cursor: pointer; } .alerts-pager-button cds-icon { - color: #fff; - color: var(--clr-app-alert-pager-text-color, #fff); + color: #fff; + color: var(--clr-app-alert-pager-text-color, #fff); } .alerts-pager-control { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - margin-top: 0.3rem; - white-space: nowrap; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + margin-top: 0.3rem; + white-space: nowrap; } .alerts-page-down { - margin-left: 1.2rem; - width: 33.33%; + margin-left: 1.2rem; + width: 33.33%; } .alerts-page-up { - margin-right: 1.2rem; - width: 33.33%; + margin-right: 1.2rem; + width: 33.33%; } .alerts-pager-text { - width: 33.33%; + width: 33.33%; } :root { - --clr-card-bg-color: var(--clr-color-neutral-0); - --clr-card-divider-color: var(--clr-color-neutral-300); - --clr-card-title-color: var(--clr-h4-color); - --clr-card-title-font-weight: var(--clr-h4-font-weight); - --clr-card-border-width: var(--clr-global-borderwidth); - --clr-card-border-radius: var(--clr-global-borderradius); - --clr-card-border-color: var(--clr-color-neutral-300); - --clr-card-box-shadow-color: var(--clr-card-border-color); - --clr-card-clickable-border-color: var(--clr-color-action-500); - --clr-card-clickable-box-shadow-color: var( - --clr-card-clickable-border-color - ); + --clr-card-bg-color: var(--clr-color-neutral-0); + --clr-card-divider-color: var(--clr-color-neutral-300); + --clr-card-title-color: var(--clr-h4-color); + --clr-card-title-font-weight: var(--clr-h4-font-weight); + --clr-card-border-width: var(--clr-global-borderwidth); + --clr-card-border-radius: var(--clr-global-borderradius); + --clr-card-border-color: var(--clr-color-neutral-300); + --clr-card-box-shadow-color: var(--clr-card-border-color); + --clr-card-clickable-border-color: var(--clr-color-action-500); + --clr-card-clickable-box-shadow-color: var(--clr-card-clickable-border-color); } .card { - -webkit-box-shadow: 0 0.15rem 0 0 #dedede; - box-shadow: 0 0.15rem 0 0 #dedede; - -webkit-box-shadow: 0 0.15rem 0 0 var(--clr-card-box-shadow-color); - box-shadow: 0 0.15rem 0 0 var(--clr-card-box-shadow-color); - border-radius: 0.15rem; - border-radius: var(--clr-card-border-radius, 0.15rem); - border-width: 0.05rem; - border-width: var(--clr-card-border-width, 0.05rem); - border-style: solid; - border-color: #dedede; - border-color: var(--clr-card-border-color, #dedede); + -webkit-box-shadow: 0 0.15rem 0 0 #dedede; + box-shadow: 0 0.15rem 0 0 #dedede; + -webkit-box-shadow: 0 0.15rem 0 0 var(--clr-card-box-shadow-color); + box-shadow: 0 0.15rem 0 0 var(--clr-card-box-shadow-color); + border-radius: 0.15rem; + border-radius: var(--clr-card-border-radius, 0.15rem); + border-width: 0.05rem; + border-width: var(--clr-card-border-width, 0.05rem); + border-style: solid; + border-color: #dedede; + border-color: var(--clr-card-border-color, #dedede); } .card.clickable:hover { - -webkit-box-shadow: 0 0.15rem 0 0 #179bd3; - box-shadow: 0 0.15rem 0 0 #179bd3; - -webkit-box-shadow: 0 0.15rem 0 0 var(--clr-card-clickable-box-shadow-color); - box-shadow: 0 0.15rem 0 0 var(--clr-card-clickable-box-shadow-color); - border-width: 0.05rem; - border-width: var(--clr-card-border-width, 0.05rem); - border-style: solid; - border-color: #179bd3; - border-color: var(--clr-card-clickable-border-color, #179bd3); - cursor: pointer; - text-decoration: none; - -webkit-transform: translateY(-0.1rem); - transform: translateY(-0.1rem); - -webkit-transition: - border 0.2s ease, - -webkit-transform 0.2s ease; - transition: - border 0.2s ease, - -webkit-transform 0.2s ease; - transition: - border 0.2s ease, - transform 0.2s ease; - transition: - border 0.2s ease, - transform 0.2s ease, - -webkit-transform 0.2s ease; + -webkit-box-shadow: 0 0.15rem 0 0 #179bd3; + box-shadow: 0 0.15rem 0 0 #179bd3; + -webkit-box-shadow: 0 0.15rem 0 0 var(--clr-card-clickable-box-shadow-color); + box-shadow: 0 0.15rem 0 0 var(--clr-card-clickable-box-shadow-color); + border-width: 0.05rem; + border-width: var(--clr-card-border-width, 0.05rem); + border-style: solid; + border-color: #179bd3; + border-color: var(--clr-card-clickable-border-color, #179bd3); + cursor: pointer; + text-decoration: none; + -webkit-transform: translateY(-0.1rem); + transform: translateY(-0.1rem); + -webkit-transition: + border 0.2s ease, + -webkit-transform 0.2s ease; + transition: + border 0.2s ease, + -webkit-transform 0.2s ease; + transition: + border 0.2s ease, + transform 0.2s ease; + transition: + border 0.2s ease, + transform 0.2s ease, + -webkit-transform 0.2s ease; } .card .card-media-block, .card .card-text, @@ -6040,8 +6011,8 @@ button.alerts-pager-button { .card .list, .card .list-unstyled, .card-block .card-divider { - margin-top: 0; - margin-bottom: 0.6rem; + margin-top: 0; + margin-bottom: 0.6rem; } .card .card-media-block:last-child, .card .card-text:last-child, @@ -6049,92 +6020,92 @@ button.alerts-pager-button { .card .list-unstyled:last-child, .card .list:last-child, .card-block .card-divider:last-child { - margin-bottom: 0; + margin-bottom: 0; } .card-img > img, .card.card-img > img, .card > .card-img:first-child:last-child > img { - display: block; - height: auto; - width: 100%; - max-width: 100%; + display: block; + height: auto; + width: 100%; + max-width: 100%; } .card { - position: relative; - display: block; - background-color: #fff; - background-color: var(--clr-card-bg-color, #fff); - width: 100%; - margin-top: 1.2rem; + position: relative; + display: block; + background-color: #fff; + background-color: var(--clr-card-bg-color, #fff); + width: 100%; + margin-top: 1.2rem; } .card .btn-link { - min-width: 0; - padding: 0; + min-width: 0; + padding: 0; } .card.clickable { - color: inherit; + color: inherit; } .card > .list, .card > .list-unstyled { - padding: 0.6rem 0.9rem; + padding: 0.6rem 0.9rem; } .card .list-group { - padding-left: 0; - margin-bottom: 0; - list-style: none; + padding-left: 0; + margin-bottom: 0; + list-style: none; } .card .list-group-item { - padding: 0.6rem 0.9rem; - font-size: 0.7rem; - background-color: #fff; - background-color: var(--clr-card-bg-color, #fff); - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-card-border-width, 0.05rem); - border-bottom-style: solid; - border-bottom-color: #dedede; - border-bottom-color: var(--clr-card-border-color, #dedede); + padding: 0.6rem 0.9rem; + font-size: 0.7rem; + background-color: #fff; + background-color: var(--clr-card-bg-color, #fff); + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-card-border-width, 0.05rem); + border-bottom-style: solid; + border-bottom-color: #dedede; + border-bottom-color: var(--clr-card-border-color, #dedede); } @supports (-ms-ime-align: auto) { - .card .dropdown > .dropdown-toggle::after { - display: inline-block; - margin-top: -0.6rem; - } + .card .dropdown > .dropdown-toggle::after { + display: inline-block; + margin-top: -0.6rem; + } } .card-block, .card-footer, .card-header { - padding: 0.6rem 0.9rem; + padding: 0.6rem 0.9rem; } .card-header, .card-title { - color: #000; - color: var(--clr-card-title-color, #000); - font-weight: 200; - font-weight: var(--clr-card-title-font-weight, 200); - font-size: 0.9rem; - letter-spacing: normal; + color: #000; + color: var(--clr-card-title-color, #000); + font-weight: 200; + font-weight: var(--clr-card-title-font-weight, 200); + font-size: 0.9rem; + letter-spacing: normal; } .card-text { - font-size: 0.7rem; + font-size: 0.7rem; } .card-img:first-child > img { - border-radius: 0; - border-top-left-radius: 0.15rem; - border-top-left-radius: var(--clr-card-border-radius, 0.15rem); - border-top-right-radius: 0.15rem; - border-top-right-radius: var(--clr-card-border-radius, 0.15rem); + border-radius: 0; + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-card-border-radius, 0.15rem); + border-top-right-radius: 0.15rem; + border-top-right-radius: var(--clr-card-border-radius, 0.15rem); } .card-img:last-child > img { - border-radius: 0; - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-card-border-radius, 0.15rem); - border-bottom-right-radius: 0.15rem; - border-bottom-right-radius: var(--clr-card-border-radius, 0.15rem); + border-radius: 0; + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-card-border-radius, 0.15rem); + border-bottom-right-radius: 0.15rem; + border-bottom-right-radius: var(--clr-card-border-radius, 0.15rem); } .card.card-img > img, .card > .card-img:first-child:last-child > img { - border-radius: 0.15rem; - border-radius: var(--clr-card-border-radius, 0.15rem); + border-radius: 0.15rem; + border-radius: var(--clr-card-border-radius, 0.15rem); } .card-block .btn, .card-block .btn.btn-link, @@ -6142,144 +6113,144 @@ button.alerts-pager-button { .card-footer .btn, .card-footer .btn.btn-link, .card-footer .card-link { - margin: 0 0.6rem 0 0; + margin: 0 0.6rem 0 0; } .card-block .btn-group .btn, .card-footer .btn-group .btn { - margin: 0; + margin: 0; } .card-block, .card-header { - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-card-border-width, 0.05rem); - border-bottom-style: solid; - border-bottom-color: #dedede; - border-bottom-color: var(--clr-card-border-color, #dedede); + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-card-border-width, 0.05rem); + border-bottom-style: solid; + border-bottom-color: #dedede; + border-bottom-color: var(--clr-card-border-color, #dedede); } .card-block:last-child, .card-header:last-child { - border-bottom: none; + border-bottom: none; } .card-divider { - display: block; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-card-border-width, 0.05rem); - border-bottom-style: solid; - border-bottom-color: #dedede; - border-bottom-color: var(--clr-card-divider-color, #dedede); + display: block; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-card-border-width, 0.05rem); + border-bottom-style: solid; + border-bottom-color: #dedede; + border-bottom-color: var(--clr-card-divider-color, #dedede); } .card-block .card-divider { - margin-left: -0.9rem; - margin-right: -0.9rem; - width: auto; + margin-left: -0.9rem; + margin-right: -0.9rem; + width: auto; } .card-block + .card-divider, .card-header + .card-divider { - display: none; + display: none; } .card-media-block { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .card-media-block .card-media-image { - display: inline-block; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - height: 3rem; - width: 3rem; - max-height: 3rem; - max-width: 3rem; + display: inline-block; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + height: 3rem; + width: 3rem; + max-height: 3rem; + max-width: 3rem; } .card-media-block .card-media-description { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - margin: 0 0 0 0.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + margin: 0 0 0 0.6rem; } .card-media-block .card-media-title { - display: inline-block; + display: inline-block; } .card-media-block .card-media-text, .card-media-block span { - display: inline-block; + display: inline-block; } .card-media-block.wrap { - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .card-media-block.wrap .card-media-description { - margin: 0.3rem 0 0 0; + margin: 0.3rem 0 0 0; } .card-block > .list, .card-block > .list-unstyled { - padding: 0; + padding: 0; } @media screen and (min-width: 576px) { - .card-columns { - -webkit-column-count: 3; - -moz-column-count: 3; - column-count: 3; - -webkit-column-gap: 0.6rem; - -moz-column-gap: 0.6rem; - column-gap: 0.6rem; - -webkit-column-break-inside: avoid; - -moz-column-break-inside: avoid; - break-inside: avoid; - -webkit-column-fill: balance; - -moz-column-fill: balance; - column-fill: balance; - -webkit-perspective: 1; - } - .card-columns.card-columns-2 { - -webkit-column-count: 2; - -moz-column-count: 2; - column-count: 2; - } - .card-columns.card-columns-4 { - -webkit-column-count: 4; - -moz-column-count: 4; - column-count: 4; - } - .card-columns .card { - display: inline-block; - margin: 0.3rem; - } - .card-columns .clickable { - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - } + .card-columns { + -webkit-column-count: 3; + -moz-column-count: 3; + column-count: 3; + -webkit-column-gap: 0.6rem; + -moz-column-gap: 0.6rem; + column-gap: 0.6rem; + -webkit-column-break-inside: avoid; + -moz-column-break-inside: avoid; + break-inside: avoid; + -webkit-column-fill: balance; + -moz-column-fill: balance; + column-fill: balance; + -webkit-perspective: 1; + } + .card-columns.card-columns-2 { + -webkit-column-count: 2; + -moz-column-count: 2; + column-count: 2; + } + .card-columns.card-columns-4 { + -webkit-column-count: 4; + -moz-column-count: 4; + column-count: 4; + } + .card-columns .card { + display: inline-block; + margin: 0.3rem; + } + .card-columns .clickable { + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + } } @supports (-ms-ime-align: auto) { - .card .checkbox-inline.btn label, - .card .checkbox.btn label, - .card .radio-inline.btn label, - .card .radio.btn label { - display: inline-block; - } + .card .checkbox-inline.btn label, + .card .checkbox.btn label, + .card .radio-inline.btn label, + .card .radio.btn label { + display: inline-block; + } } :root { - --clr-dropdown-active-text-color: var(--clr-color-neutral-1000); - --clr-dropdown-bg-color: var(--clr-color-neutral-0); - --clr-dropdown-border-color: var(--clr-color-neutral-400); - --clr-dropdown-divider-color: var(--clr-color-neutral-200); - --clr-dropdown-divider-border-width: var(--clr-global-borderwidth); - --clr-dropdown-child-border-color: var(--clr-color-neutral-500); - --clr-dropdown-bg-hover-color: var(--clr-global-hover-color); - --clr-dropdown-selection-color: var(--clr-global-selection-color); - --clr-dropdown-box-shadow: var(--clr-popover-box-shadow-color); - --clr-dropdown-text-color: var(--clr-p1-color); - --clr-dropdown-header-color: var(--clr-color-neutral-900); - --clr-dropdown-header-font-weight: 600; - --clr-dropdown-item-color: var(--clr-color-neutral-700); - --clr-dropdown-item-font-weight: var(--clr-p1-font-weight); + --clr-dropdown-active-text-color: var(--clr-color-neutral-1000); + --clr-dropdown-bg-color: var(--clr-color-neutral-0); + --clr-dropdown-border-color: var(--clr-color-neutral-400); + --clr-dropdown-divider-color: var(--clr-color-neutral-200); + --clr-dropdown-divider-border-width: var(--clr-global-borderwidth); + --clr-dropdown-child-border-color: var(--clr-color-neutral-500); + --clr-dropdown-bg-hover-color: var(--clr-global-hover-color); + --clr-dropdown-selection-color: var(--clr-global-selection-color); + --clr-dropdown-box-shadow: var(--clr-popover-box-shadow-color); + --clr-dropdown-text-color: var(--clr-p1-color); + --clr-dropdown-header-color: var(--clr-color-neutral-900); + --clr-dropdown-header-font-weight: 600; + --clr-dropdown-item-color: var(--clr-color-neutral-700); + --clr-dropdown-item-font-weight: var(--clr-p1-font-weight); } .dropdown-menu .btn, .dropdown-menu .btn-danger, @@ -6297,101 +6268,101 @@ button.alerts-pager-button { .dropdown-menu .btn-warning, .dropdown-menu .dropdown-header, .dropdown-menu .dropdown-item { - overflow: hidden; - text-overflow: ellipsis; - text-align: left; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; } .dropdown { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .dropdown .dropdown-toggle { - display: inline-block; - position: relative; - margin: 0; - white-space: nowrap; - cursor: pointer; + display: inline-block; + position: relative; + margin: 0; + white-space: nowrap; + cursor: pointer; } .dropdown .dropdown-toggle > * { - margin: 0; + margin: 0; } -.dropdown .dropdown-toggle cds-icon[shape^='angle'] { - position: absolute; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - color: inherit; - height: 0.5rem; - width: 0.5rem; +.dropdown .dropdown-toggle cds-icon[shape^="angle"] { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + color: inherit; + height: 0.5rem; + width: 0.5rem; } .dropdown .dropdown-toggle.btn { - padding-right: 1.2rem; + padding-right: 1.2rem; } -.dropdown .dropdown-toggle.btn cds-icon[shape^='angle'] { - right: 0.6rem; +.dropdown .dropdown-toggle.btn cds-icon[shape^="angle"] { + right: 0.6rem; } .dropdown .dropdown-toggle:not(.btn) { - padding: 0 0.6rem 0 0; - color: #000; - color: var(--clr-dropdown-active-text-color, #000); + padding: 0 0.6rem 0 0; + color: #000; + color: var(--clr-dropdown-active-text-color, #000); } -.dropdown .dropdown-toggle:not(.btn) cds-icon[shape^='angle'] { - right: 0; +.dropdown .dropdown-toggle:not(.btn) cds-icon[shape^="angle"] { + right: 0; } .dropdown button.dropdown-toggle:not(.btn) { - background: 0 0; - border: none; - cursor: pointer; - color: #000; - color: var(--clr-dropdown-active-text-color, #000); + background: 0 0; + border: none; + cursor: pointer; + color: #000; + color: var(--clr-dropdown-active-text-color, #000); } .dropdown-menu > * { - display: block; - white-space: nowrap; + display: block; + white-space: nowrap; } .dropdown-menu { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - position: absolute; - top: 100%; - left: 0; - min-width: 6rem; - max-width: 18rem; - background: #fff; - background: var(--clr-dropdown-bg-color, #fff); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-dropdown-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); - margin-top: 0.1rem; - padding: 0.6rem 0; - visibility: hidden; - z-index: 1060; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + position: absolute; + top: 100%; + left: 0; + min-width: 6rem; + max-width: 18rem; + background: #fff; + background: var(--clr-dropdown-bg-color, #fff); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-dropdown-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); + margin-top: 0.1rem; + padding: 0.6rem 0; + visibility: hidden; + z-index: 1060; } .dropdown-menu .dropdown-header { - color: #333; - color: var(--clr-dropdown-header-color, #333); - font-size: 0.6rem; - font-weight: 600; - font-weight: var(--clr-dropdown-header-font-weight, 600); - letter-spacing: normal; - padding: 0 0.6rem; - line-height: 0.9rem; - margin: 0; + color: #333; + color: var(--clr-dropdown-header-color, #333); + font-size: 0.6rem; + font-weight: 600; + font-weight: var(--clr-dropdown-header-font-weight, 600); + letter-spacing: normal; + padding: 0 0.6rem; + line-height: 0.9rem; + margin: 0; } .dropdown-menu .btn, .dropdown-menu .btn-danger, @@ -6408,21 +6379,21 @@ button.alerts-pager-button { .dropdown-menu .btn-success, .dropdown-menu .btn-warning, .dropdown-menu .dropdown-item { - color: #666; - color: var(--clr-dropdown-item-color, #666); - font-size: 0.7rem; - font-weight: 400; - font-weight: var(--clr-dropdown-item-font-weight, 400); - letter-spacing: normal; - background: 0 0; - border: 0; - cursor: pointer; - display: block; - height: auto; - line-height: inherit; - margin: 0; - width: 100%; - text-transform: none; + color: #666; + color: var(--clr-dropdown-item-color, #666); + font-size: 0.7rem; + font-weight: 400; + font-weight: var(--clr-dropdown-item-font-weight, 400); + letter-spacing: normal; + background: 0 0; + border: 0; + cursor: pointer; + display: block; + height: auto; + line-height: inherit; + margin: 0; + width: 100%; + text-transform: none; } .dropdown-menu .btn-danger:hover, .dropdown-menu .btn-info:hover, @@ -6439,11 +6410,11 @@ button.alerts-pager-button { .dropdown-menu .btn-warning:hover, .dropdown-menu .btn:hover, .dropdown-menu .dropdown-item:hover { - background-color: #e8e8e8; - background-color: var(--clr-dropdown-bg-hover-color, #e8e8e8); - color: #666; - color: var(--clr-dropdown-item-color, #666); - text-decoration: none; + background-color: #e8e8e8; + background-color: var(--clr-dropdown-bg-hover-color, #e8e8e8); + color: #666; + color: var(--clr-dropdown-item-color, #666); + text-decoration: none; } .dropdown-menu .btn-danger.active, .dropdown-menu .btn-info.active, @@ -6460,10 +6431,10 @@ button.alerts-pager-button { .dropdown-menu .btn-warning.active, .dropdown-menu .btn.active, .dropdown-menu .dropdown-item.active { - background: #d8e3e9; - background: var(--clr-dropdown-selection-color, #d8e3e9); - color: #000; - color: var(--clr-dropdown-active-text-color, #000); + background: #d8e3e9; + background: var(--clr-dropdown-selection-color, #d8e3e9); + color: #000; + color: var(--clr-dropdown-active-text-color, #000); } .dropdown-menu .btn-danger:active, .dropdown-menu .btn-info:active, @@ -6480,8 +6451,8 @@ button.alerts-pager-button { .dropdown-menu .btn-warning:active, .dropdown-menu .btn:active, .dropdown-menu .dropdown-item:active { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .dropdown-menu .btn-danger:focus, .dropdown-menu .btn-info:focus, @@ -6498,7 +6469,7 @@ button.alerts-pager-button { .dropdown-menu .btn-warning:focus, .dropdown-menu .btn:focus, .dropdown-menu .dropdown-item:focus { - z-index: inherit; + z-index: inherit; } .dropdown-menu .btn-danger.disabled, .dropdown-menu .btn-danger:disabled, @@ -6530,12 +6501,12 @@ button.alerts-pager-button { .dropdown-menu .btn:disabled, .dropdown-menu .dropdown-item.disabled, .dropdown-menu .dropdown-item:disabled { - cursor: not-allowed; - opacity: 0.4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: not-allowed; + opacity: 0.4; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .dropdown-menu .btn-danger.disabled:hover, .dropdown-menu .btn-danger:disabled:hover, @@ -6567,7 +6538,7 @@ button.alerts-pager-button { .dropdown-menu .btn:disabled:hover, .dropdown-menu .dropdown-item.disabled:hover, .dropdown-menu .dropdown-item:disabled:hover { - background: 0 0; + background: 0 0; } .dropdown-menu .btn-danger.disabled:active, .dropdown-menu .btn-danger:disabled:active, @@ -6599,9 +6570,9 @@ button.alerts-pager-button { .dropdown-menu .btn:disabled:active, .dropdown-menu .dropdown-item.disabled:active, .dropdown-menu .dropdown-item:disabled:active { - background: 0 0; - -webkit-box-shadow: none; - box-shadow: none; + background: 0 0; + -webkit-box-shadow: none; + box-shadow: none; } .dropdown-menu .btn-danger.expandable, .dropdown-menu .btn-info.expandable, @@ -6618,8 +6589,8 @@ button.alerts-pager-button { .dropdown-menu .btn-warning.expandable, .dropdown-menu .btn.expandable, .dropdown-menu .dropdown-item.expandable { - margin-right: 1.2rem; - padding-right: 0.6rem; + margin-right: 1.2rem; + padding-right: 0.6rem; } .dropdown-menu .btn-danger.expandable:before, .dropdown-menu .btn-info.expandable:before, @@ -6636,36 +6607,36 @@ button.alerts-pager-button { .dropdown-menu .btn-warning.expandable:before, .dropdown-menu .btn.expandable:before, .dropdown-menu .dropdown-item.expandable:before { - content: ''; - float: right; - height: 0.6rem; - width: 0.6rem; - -webkit-transform: rotate(-90deg); - transform: rotate(-90deg); - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%239a9a9a%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A'); - background-repeat: no-repeat; - background-size: contain; - vertical-align: middle; - margin-top: 0.3rem; + content: ""; + float: right; + height: 0.6rem; + width: 0.6rem; + -webkit-transform: rotate(-90deg); + transform: rotate(-90deg); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%239a9a9a%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A"); + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; + margin-top: 0.3rem; } .dropdown-menu .btn, .dropdown-menu .dropdown-item { - padding: 0.15rem 1.2rem; + padding: 0.15rem 1.2rem; } @media screen and (max-width: 576px) { - .dropdown-menu .btn, - .dropdown-menu .dropdown-item { - padding: 0.3rem 1.2rem; - } + .dropdown-menu .btn, + .dropdown-menu .dropdown-item { + padding: 0.3rem 1.2rem; + } } .dropdown-menu .dropdown-divider { - font-size: 0.6rem; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-dropdown-divider-border-width, 0.05rem); - border-bottom-style: solid; - border-bottom-color: #e8e8e8; - border-bottom-color: var(--clr-dropdown-divider-color, #e8e8e8); - margin: 0.3rem 0; + font-size: 0.6rem; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-dropdown-divider-border-width, 0.05rem); + border-bottom-style: solid; + border-bottom-color: #e8e8e8; + border-bottom-color: var(--clr-dropdown-divider-color, #e8e8e8); + margin: 0.3rem 0; } .btn-group-overflow.open > .dropdown-menu, .btn-group-overflow.open > .dropdown-menu-wrapper > .dropdown-menu, @@ -6673,7 +6644,7 @@ button.alerts-pager-button { .dropdown.open > .dropdown-menu-wrapper > .dropdown-menu, .tabs-overflow.open > .dropdown-menu, .tabs-overflow.open > .dropdown-menu-wrapper > .dropdown-menu { - visibility: visible; + visibility: visible; } .btn-group-overflow.bottom-left > .dropdown-menu, .btn-group-overflow.bottom-right > .dropdown-menu, @@ -6681,21 +6652,21 @@ button.alerts-pager-button { .dropdown.bottom-right > .dropdown-menu, .tabs-overflow.bottom-left > .dropdown-menu, .tabs-overflow.bottom-right > .dropdown-menu { - top: 100%; - bottom: auto; - margin: 0.1rem 0 0 0; + top: 100%; + bottom: auto; + margin: 0.1rem 0 0 0; } .btn-group-overflow.bottom-left > .dropdown-menu, .dropdown.bottom-left > .dropdown-menu, .tabs-overflow.bottom-left > .dropdown-menu { - left: 0; - right: auto; + left: 0; + right: auto; } .btn-group-overflow.bottom-right > .dropdown-menu, .dropdown.bottom-right > .dropdown-menu, .tabs-overflow.bottom-right > .dropdown-menu { - right: 0; - left: auto; + right: 0; + left: auto; } .btn-group-overflow.top-left > .dropdown-menu, .btn-group-overflow.top-right > .dropdown-menu, @@ -6703,21 +6674,21 @@ button.alerts-pager-button { .dropdown.top-right > .dropdown-menu, .tabs-overflow.top-left > .dropdown-menu, .tabs-overflow.top-right > .dropdown-menu { - top: auto; - bottom: 100%; - margin: 0 0 0.1rem 0; + top: auto; + bottom: 100%; + margin: 0 0 0.1rem 0; } .btn-group-overflow.top-left > .dropdown-menu, .dropdown.top-left > .dropdown-menu, .tabs-overflow.top-left > .dropdown-menu { - left: 0; - right: auto; + left: 0; + right: auto; } .btn-group-overflow.top-right > .dropdown-menu, .dropdown.top-right > .dropdown-menu, .tabs-overflow.top-right > .dropdown-menu { - right: 0; - left: auto; + right: 0; + left: auto; } .btn-group-overflow.left-bottom > .dropdown-menu, .btn-group-overflow.left-top > .dropdown-menu, @@ -6725,21 +6696,21 @@ button.alerts-pager-button { .dropdown.left-top > .dropdown-menu, .tabs-overflow.left-bottom > .dropdown-menu, .tabs-overflow.left-top > .dropdown-menu { - right: 100%; - left: auto; - margin: 0 0.1rem 0 0; + right: 100%; + left: auto; + margin: 0 0.1rem 0 0; } .btn-group-overflow.left-bottom > .dropdown-menu, .dropdown.left-bottom > .dropdown-menu, .tabs-overflow.left-bottom > .dropdown-menu { - top: 0; - bottom: auto; + top: 0; + bottom: auto; } .btn-group-overflow.left-top > .dropdown-menu, .dropdown.left-top > .dropdown-menu, .tabs-overflow.left-top > .dropdown-menu { - bottom: 0; - top: auto; + bottom: 0; + top: auto; } .btn-group-overflow.right-bottom > .dropdown-menu, .btn-group-overflow.right-top > .dropdown-menu, @@ -6747,280 +6718,280 @@ button.alerts-pager-button { .dropdown.right-top > .dropdown-menu, .tabs-overflow.right-bottom > .dropdown-menu, .tabs-overflow.right-top > .dropdown-menu { - left: 100%; - right: auto; - margin: 0 0 0 0.1rem; + left: 100%; + right: auto; + margin: 0 0 0 0.1rem; } .btn-group-overflow.right-bottom > .dropdown-menu, .dropdown.right-bottom > .dropdown-menu, .tabs-overflow.right-bottom > .dropdown-menu { - top: 0; - bottom: auto; + top: 0; + bottom: auto; } .btn-group-overflow.right-top > .dropdown-menu, .dropdown.right-top > .dropdown-menu, .tabs-overflow.right-top > .dropdown-menu { - bottom: 0; - top: auto; + bottom: 0; + top: auto; } .btn-group-overflow .dropdown .dropdown-menu, .dropdown .dropdown .dropdown-menu, .tabs-overflow .dropdown .dropdown-menu { - border-color: #b3b3b3; - border-color: var(--clr-dropdown-child-border-color, #b3b3b3); - position: absolute; + border-color: #b3b3b3; + border-color: var(--clr-dropdown-child-border-color, #b3b3b3); + position: absolute; } .btn-group-overflow .dropdown.left-top > .dropdown-menu, .btn-group-overflow - .dropdown.left-top - > .dropdown-menu-wrapper - > .dropdown-menu, + .dropdown.left-top + > .dropdown-menu-wrapper + > .dropdown-menu, .dropdown .dropdown.left-top > .dropdown-menu, .dropdown .dropdown.left-top > .dropdown-menu-wrapper > .dropdown-menu, .tabs-overflow .dropdown.left-top > .dropdown-menu, .tabs-overflow .dropdown.left-top > .dropdown-menu-wrapper > .dropdown-menu { - top: 0; - bottom: auto; - left: auto; - right: 100%; - margin-top: -0.95rem; - margin-right: -0.2rem; + top: 0; + bottom: auto; + left: auto; + right: 100%; + margin-top: -0.95rem; + margin-right: -0.2rem; } .btn-group-overflow .dropdown.right-top > .dropdown-menu, .btn-group-overflow - .dropdown.right-top - > .dropdown-menu-wrapper - > .dropdown-menu, + .dropdown.right-top + > .dropdown-menu-wrapper + > .dropdown-menu, .dropdown .dropdown.right-top > .dropdown-menu, .dropdown .dropdown.right-top > .dropdown-menu-wrapper > .dropdown-menu, .tabs-overflow .dropdown.right-top > .dropdown-menu, .tabs-overflow .dropdown.right-top > .dropdown-menu-wrapper > .dropdown-menu { - top: 0; - bottom: auto; - left: 100%; - right: auto; - margin-top: -0.95rem; - margin-left: -0.2rem; + top: 0; + bottom: auto; + left: 100%; + right: auto; + margin-top: -0.95rem; + margin-left: -0.2rem; } .btn-group-overflow .dropdown.left-bottom > .dropdown-menu, .btn-group-overflow - .dropdown.left-bottom - > .dropdown-menu-wrapper - > .dropdown-menu, + .dropdown.left-bottom + > .dropdown-menu-wrapper + > .dropdown-menu, .dropdown .dropdown.left-bottom > .dropdown-menu, .dropdown .dropdown.left-bottom > .dropdown-menu-wrapper > .dropdown-menu, .tabs-overflow .dropdown.left-bottom > .dropdown-menu, .tabs-overflow .dropdown.left-bottom > .dropdown-menu-wrapper > .dropdown-menu { - top: auto; - bottom: 0; - left: auto; - right: 100%; - margin-bottom: -0.95rem; - margin-right: -0.2rem; + top: auto; + bottom: 0; + left: auto; + right: 100%; + margin-bottom: -0.95rem; + margin-right: -0.2rem; } .btn-group-overflow .dropdown.right-bottom > .dropdown-menu, .btn-group-overflow - .dropdown.right-bottom - > .dropdown-menu-wrapper - > .dropdown-menu, + .dropdown.right-bottom + > .dropdown-menu-wrapper + > .dropdown-menu, .dropdown .dropdown.right-bottom > .dropdown-menu, .dropdown .dropdown.right-bottom > .dropdown-menu-wrapper > .dropdown-menu, .tabs-overflow .dropdown.right-bottom > .dropdown-menu, .tabs-overflow - .dropdown.right-bottom - > .dropdown-menu-wrapper - > .dropdown-menu { - top: auto; - bottom: 0; - left: 100%; - right: auto; - margin-bottom: -0.95rem; - margin-left: -0.2rem; + .dropdown.right-bottom + > .dropdown-menu-wrapper + > .dropdown-menu { + top: auto; + bottom: 0; + left: 100%; + right: auto; + margin-bottom: -0.95rem; + margin-left: -0.2rem; } :root { - --clr-badge-background-color: var(--clr-color-neutral-600); - --clr-badge-color: var(--clr-color-on-neutral-600); - --clr-badge-info-bg-color: var(--clr-color-action-800); - --clr-badge-info-color: var(--clr-color-neutral-0); - --clr-badge-success-bg-color: var(--clr-color-success-800); - --clr-badge-success-color: var(--clr-color-neutral-0); - --clr-badge-warning-bg-color: var(--clr-color-warning-1000); - --clr-badge-warning-color: var(--clr-color-neutral-0); - --clr-badge-danger-bg-color: var(--clr-color-danger-900); - --clr-badge-danger-color: var(--clr-color-neutral-0); - --clr-badge-gray-bg-color: var(--clr-color-neutral-600); - --clr-badge-gray-color: var(--clr-badge-font-color-light); - --clr-badge-purple-bg-color: var(--clr-color-secondary-action-500); - --clr-badge-purple-color: var(--clr-badge-font-color-light); - --clr-badge-blue-bg-color: var(--clr-color-action-800); - --clr-badge-blue-color: var(--clr-badge-font-color-light); - --clr-badge-orange-bg-color: var(--clr-color-warning-1000); - --clr-badge-orange-color: var(--clr-color-neutral-0); - --clr-badge-light-blue-bg-color: var(--clr-color-action-500); - --clr-badge-light-blue-color: var(--clr-color-neutral-0); + --clr-badge-background-color: var(--clr-color-neutral-600); + --clr-badge-color: var(--clr-color-on-neutral-600); + --clr-badge-info-bg-color: var(--clr-color-action-800); + --clr-badge-info-color: var(--clr-color-neutral-0); + --clr-badge-success-bg-color: var(--clr-color-success-800); + --clr-badge-success-color: var(--clr-color-neutral-0); + --clr-badge-warning-bg-color: var(--clr-color-warning-1000); + --clr-badge-warning-color: var(--clr-color-neutral-0); + --clr-badge-danger-bg-color: var(--clr-color-danger-900); + --clr-badge-danger-color: var(--clr-color-neutral-0); + --clr-badge-gray-bg-color: var(--clr-color-neutral-600); + --clr-badge-gray-color: var(--clr-badge-font-color-light); + --clr-badge-purple-bg-color: var(--clr-color-secondary-action-500); + --clr-badge-purple-color: var(--clr-badge-font-color-light); + --clr-badge-blue-bg-color: var(--clr-color-action-800); + --clr-badge-blue-color: var(--clr-badge-font-color-light); + --clr-badge-orange-bg-color: var(--clr-color-warning-1000); + --clr-badge-orange-color: var(--clr-color-neutral-0); + --clr-badge-light-blue-bg-color: var(--clr-color-action-500); + --clr-badge-light-blue-color: var(--clr-color-neutral-0); } .badge { - display: -webkit-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; - vertical-align: middle; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - min-width: 0.75rem; - background: #8c8c8c; - height: 0.75rem; - line-height: normal; - border-radius: 0.5rem; - font-size: 0.5rem; - padding: 0 0.2rem; - margin-right: 0.3rem; - white-space: nowrap; - text-align: center; - color: #fff; - color: var(--clr-badge-font-color-light, #fff); + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + vertical-align: middle; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + min-width: 0.75rem; + background: #8c8c8c; + height: 0.75rem; + line-height: normal; + border-radius: 0.5rem; + font-size: 0.5rem; + padding: 0 0.2rem; + margin-right: 0.3rem; + white-space: nowrap; + text-align: center; + color: #fff; + color: var(--clr-badge-font-color-light, #fff); } .badge:visited { - color: #fff; - color: var(--clr-badge-font-color-light, #fff); + color: #fff; + color: var(--clr-badge-font-color-light, #fff); } .badge.badge-1, .badge.badge-gray { - background: #8c8c8c; - background: var(--clr-badge-gray-bg-color, #8c8c8c); - color: #fff; - color: var(--clr-badge-gray-color, #fff); + background: #8c8c8c; + background: var(--clr-badge-gray-bg-color, #8c8c8c); + color: #fff; + color: var(--clr-badge-gray-color, #fff); } .badge.badge-2, .badge.badge-purple { - background: #9e57bc; - background: var(--clr-badge-purple-bg-color, #9e57bc); - color: #fff; - color: var(--clr-badge-purple-color, #fff); + background: #9e57bc; + background: var(--clr-badge-purple-bg-color, #9e57bc); + color: #fff; + color: var(--clr-badge-purple-color, #fff); } .badge.badge-3, .badge.badge-blue { - background: #00567a; - background: var(--clr-badge-blue-bg-color, #00567a); - color: #fff; - color: var(--clr-badge-blue-color, #fff); + background: #00567a; + background: var(--clr-badge-blue-bg-color, #00567a); + color: #fff; + color: var(--clr-badge-blue-color, #fff); } .badge.badge-4, .badge.badge-orange { - background: #613200; - background: var(--clr-badge-orange-bg-color, #613200); - color: #fff; - color: var(--clr-badge-orange-color, #fff); + background: #613200; + background: var(--clr-badge-orange-bg-color, #613200); + color: #fff; + color: var(--clr-badge-orange-color, #fff); } .badge.badge-5, .badge.badge-light-blue { - background: #179bd3; - background: var(--clr-badge-light-blue-bg-color, #179bd3); - color: #fff; - color: var(--clr-badge-light-blue-color, #fff); + background: #179bd3; + background: var(--clr-badge-light-blue-bg-color, #179bd3); + color: #fff; + color: var(--clr-badge-light-blue-color, #fff); } .badge.badge-info { - background: #00567a; - background: var(--clr-badge-info-bg-color, #00567a); - color: #fff; - color: var(--clr-badge-info-color, #fff); + background: #00567a; + background: var(--clr-badge-info-bg-color, #00567a); + color: #fff; + color: var(--clr-badge-info-color, #fff); } .badge.badge-success { - background: #306b00; - background: var(--clr-badge-success-bg-color, #306b00); - color: #fff; - color: var(--clr-badge-success-color, #fff); + background: #306b00; + background: var(--clr-badge-success-bg-color, #306b00); + color: #fff; + color: var(--clr-badge-success-color, #fff); } .badge.badge-danger { - background: #991700; - background: var(--clr-badge-danger-bg-color, #991700); - color: #fff; - color: var(--clr-badge-danger-color, #fff); + background: #991700; + background: var(--clr-badge-danger-bg-color, #991700); + color: #fff; + color: var(--clr-badge-danger-color, #fff); } .badge.badge-warning { - background: #613200; - background: var(--clr-badge-warning-bg-color, #613200); - color: #fff; - color: var(--clr-badge-warning-color, #fff); + background: #613200; + background: var(--clr-badge-warning-bg-color, #613200); + color: #fff; + color: var(--clr-badge-warning-color, #fff); } _:-ms-input-placeholder .badge, :root .badge { - padding: 0.1rem 0.15rem 0; + padding: 0.1rem 0.15rem 0; } @supports (-ms-ime-align: auto) { - .badge { - padding: 0.1rem 0.15rem 0; - } -} -:root { - --clr-label-font-color-light: var(--clr-color-neutral-1000); - --clr-label-font-color-dark: var(--clr-color-neutral-1000); - --clr-label-default-border-color: var(--clr-color-neutral-600); - --clr-label-font-size: 0.55rem; - --clr-label-font-weight: 400; - --clr-label-letter-spacing: 0.03em; - --clr-label-border-radius: 0.6rem; - --clr-label-bg-hover-color: var(--clr-color-neutral-200); - --clr-label-gray-bg-color: var(--clr-color-neutral-600); - --clr-label-gray-color: var(--clr-label-font-color-light); - --clr-label-purple-bg-color: var(--clr-color-secondary-action-500); - --clr-label-purple-color: var(--clr-label-font-color-light); - --clr-label-blue-bg-color: var(--clr-color-action-800); - --clr-label-blue-color: var(--clr-label-font-color-light); - --clr-label-orange-bg-color: var(--clr-color-warning-1000); - --clr-label-orange-color: var(--clr-label-font-color-dark); - --clr-label-light-blue-bg-color: var(--clr-color-action-500); - --clr-label-light-blue-color: var(--clr-label-font-color-dark); - --clr-label-info-bg-color: var(--clr-color-action-50); - --clr-label-info-font-color: var(--clr-color-action-800); - --clr-label-info-border-color: var(--clr-color-action-800); - --clr-label-success-bg-color: var(--clr-color-success-50); - --clr-label-success-font-color: var(--clr-color-success-800); - --clr-label-success-border-color: var(--clr-color-success-800); - --clr-label-warning-bg-color: var(--clr-color-warning-100); - --clr-label-warning-font-color: var(--clr-color-neutral-900); - --clr-label-warning-border-color: var(--clr-color-warning-800); - --clr-label-danger-bg-color: var(--clr-color-danger-100); - --clr-label-danger-font-color: var(--clr-color-danger-900); - --clr-label-danger-border-color: var(--clr-color-danger-900); -} -.label, -a.label { - font-size: 0.55rem; - font-size: var(--clr-label-font-size, 0.55rem); - font-weight: 400; - font-weight: var(--clr-label-font-weight, 400); - letter-spacing: 0.03em; - letter-spacing: var(--clr-label-letter-spacing, 0.03em); - line-height: 0.6rem; - display: -webkit-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - padding: 0 0.6rem 0.05rem; - border-radius: 0.6rem; - border-radius: var(--clr-label-border-radius, 0.6rem); - border: 0.05rem solid; - border-color: #8c8c8c; - border-color: var(--clr-label-default-border-color, #8c8c8c); - height: 1.05rem; - margin: 0 0.3rem 0 0; - white-space: nowrap; - color: #000; - color: var(--clr-label-font-color-light, #000); + .badge { + padding: 0.1rem 0.15rem 0; + } +} +:root { + --clr-label-font-color-light: var(--clr-color-neutral-1000); + --clr-label-font-color-dark: var(--clr-color-neutral-1000); + --clr-label-default-border-color: var(--clr-color-neutral-600); + --clr-label-font-size: 0.55rem; + --clr-label-font-weight: 400; + --clr-label-letter-spacing: 0.03em; + --clr-label-border-radius: 0.6rem; + --clr-label-bg-hover-color: var(--clr-color-neutral-200); + --clr-label-gray-bg-color: var(--clr-color-neutral-600); + --clr-label-gray-color: var(--clr-label-font-color-light); + --clr-label-purple-bg-color: var(--clr-color-secondary-action-500); + --clr-label-purple-color: var(--clr-label-font-color-light); + --clr-label-blue-bg-color: var(--clr-color-action-800); + --clr-label-blue-color: var(--clr-label-font-color-light); + --clr-label-orange-bg-color: var(--clr-color-warning-1000); + --clr-label-orange-color: var(--clr-label-font-color-dark); + --clr-label-light-blue-bg-color: var(--clr-color-action-500); + --clr-label-light-blue-color: var(--clr-label-font-color-dark); + --clr-label-info-bg-color: var(--clr-color-action-50); + --clr-label-info-font-color: var(--clr-color-action-800); + --clr-label-info-border-color: var(--clr-color-action-800); + --clr-label-success-bg-color: var(--clr-color-success-50); + --clr-label-success-font-color: var(--clr-color-success-800); + --clr-label-success-border-color: var(--clr-color-success-800); + --clr-label-warning-bg-color: var(--clr-color-warning-100); + --clr-label-warning-font-color: var(--clr-color-neutral-900); + --clr-label-warning-border-color: var(--clr-color-warning-800); + --clr-label-danger-bg-color: var(--clr-color-danger-100); + --clr-label-danger-font-color: var(--clr-color-danger-900); + --clr-label-danger-border-color: var(--clr-color-danger-900); +} +.label, +a.label { + font-size: 0.55rem; + font-size: var(--clr-label-font-size, 0.55rem); + font-weight: 400; + font-weight: var(--clr-label-font-weight, 400); + letter-spacing: 0.03em; + letter-spacing: var(--clr-label-letter-spacing, 0.03em); + line-height: 0.6rem; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 0 0.6rem 0.05rem; + border-radius: 0.6rem; + border-radius: var(--clr-label-border-radius, 0.6rem); + border: 0.05rem solid; + border-color: #8c8c8c; + border-color: var(--clr-label-default-border-color, #8c8c8c); + height: 1.05rem; + margin: 0 0.3rem 0 0; + white-space: nowrap; + color: #000; + color: var(--clr-label-font-color-light, #000); } .label:visited, a.label:visited { - color: #000; - color: var(--clr-label-font-color-light, #000); + color: #000; + color: var(--clr-label-font-color-light, #000); } .label:active, .label:focus, @@ -7028,358 +6999,357 @@ a.label:visited { a.label:active, a.label:focus, a.label:hover { - text-decoration: none; + text-decoration: none; } .label.clickable:active, .label.clickable:hover, a.label.clickable:active, a.label.clickable:hover { - background: #e8e8e8; - background: var(--clr-label-bg-hover-color, #e8e8e8); + background: #e8e8e8; + background: var(--clr-label-bg-hover-color, #e8e8e8); } .label.clickable:active, a.label.clickable:active { - -webkit-box-shadow: 0 0.05rem 0 0 #8c8c8c inset; - box-shadow: 0 0.05rem 0 0 #8c8c8c inset; - -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) - inset; - box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) inset; - -webkit-transform: translateY(0.5px); - transform: translateY(0.5px); + -webkit-box-shadow: 0 0.05rem 0 0 #8c8c8c inset; + box-shadow: 0 0.05rem 0 0 #8c8c8c inset; + -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) + inset; + box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) inset; + -webkit-transform: translateY(0.5px); + transform: translateY(0.5px); } .label.label-1, .label.label-gray, a.label.label-1, a.label.label-gray { - border: 0.05rem solid; - border-color: #8c8c8c; - border-color: var(--clr-label-gray-bg-color, #8c8c8c); + border: 0.05rem solid; + border-color: #8c8c8c; + border-color: var(--clr-label-gray-bg-color, #8c8c8c); } .label.clickable.label-gray:active, .label.clickable.label-gray:hover, a.label.clickable.label-gray:active, a.label.clickable.label-gray:hover { - text-decoration: none; - background: #e8e8e8; - background: var(--clr-label-bg-hover-color, #e8e8e8); + text-decoration: none; + background: #e8e8e8; + background: var(--clr-label-bg-hover-color, #e8e8e8); } .label.clickable.label-gray:active, a.label.clickable.label-gray:active { - -webkit-box-shadow: 0 0.05rem 0 0 #8c8c8c inset; - box-shadow: 0 0.05rem 0 0 #8c8c8c inset; - -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) - inset; - box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) inset; - -webkit-transform: translateY(0.5px); - transform: translateY(0.5px); + -webkit-box-shadow: 0 0.05rem 0 0 #8c8c8c inset; + box-shadow: 0 0.05rem 0 0 #8c8c8c inset; + -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) + inset; + box-shadow: 0 0.05rem 0 0 var(--clr-label-gray-bg-color, #8c8c8c) inset; + -webkit-transform: translateY(0.5px); + transform: translateY(0.5px); } .label.label-gray > .badge, a.label.label-gray > .badge { - background: #8c8c8c; - background: var(--clr-badge-gray-bg-color, #8c8c8c); - color: #fff; - color: var(--clr-badge-gray-color, #fff); + background: #8c8c8c; + background: var(--clr-badge-gray-bg-color, #8c8c8c); + color: #fff; + color: var(--clr-badge-gray-color, #fff); } .label.label-2, .label.label-purple, a.label.label-2, a.label.label-purple { - border: 0.05rem solid; - border-color: #9e57bc; - border-color: var(--clr-label-purple-bg-color, #9e57bc); + border: 0.05rem solid; + border-color: #9e57bc; + border-color: var(--clr-label-purple-bg-color, #9e57bc); } .label.clickable.label-purple:active, .label.clickable.label-purple:hover, a.label.clickable.label-purple:active, a.label.clickable.label-purple:hover { - text-decoration: none; - background: #e8e8e8; - background: var(--clr-label-bg-hover-color, #e8e8e8); + text-decoration: none; + background: #e8e8e8; + background: var(--clr-label-bg-hover-color, #e8e8e8); } .label.clickable.label-purple:active, a.label.clickable.label-purple:active { - -webkit-box-shadow: 0 0.05rem 0 0 #9e57bc inset; - box-shadow: 0 0.05rem 0 0 #9e57bc inset; - -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-purple-bg-color, #9e57bc) - inset; - box-shadow: 0 0.05rem 0 0 var(--clr-label-purple-bg-color, #9e57bc) inset; - -webkit-transform: translateY(0.5px); - transform: translateY(0.5px); + -webkit-box-shadow: 0 0.05rem 0 0 #9e57bc inset; + box-shadow: 0 0.05rem 0 0 #9e57bc inset; + -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-purple-bg-color, #9e57bc) + inset; + box-shadow: 0 0.05rem 0 0 var(--clr-label-purple-bg-color, #9e57bc) inset; + -webkit-transform: translateY(0.5px); + transform: translateY(0.5px); } .label.label-purple > .badge, a.label.label-purple > .badge { - background: #9e57bc; - background: var(--clr-badge-purple-bg-color, #9e57bc); - color: #fff; - color: var(--clr-badge-purple-color, #fff); + background: #9e57bc; + background: var(--clr-badge-purple-bg-color, #9e57bc); + color: #fff; + color: var(--clr-badge-purple-color, #fff); } .label.label-3, .label.label-blue, a.label.label-3, a.label.label-blue { - border: 0.05rem solid; - border-color: #00567a; - border-color: var(--clr-label-blue-bg-color, #00567a); + border: 0.05rem solid; + border-color: #00567a; + border-color: var(--clr-label-blue-bg-color, #00567a); } .label.clickable.label-blue:active, .label.clickable.label-blue:hover, a.label.clickable.label-blue:active, a.label.clickable.label-blue:hover { - text-decoration: none; - background: #e8e8e8; - background: var(--clr-label-bg-hover-color, #e8e8e8); + text-decoration: none; + background: #e8e8e8; + background: var(--clr-label-bg-hover-color, #e8e8e8); } .label.clickable.label-blue:active, a.label.clickable.label-blue:active { - -webkit-box-shadow: 0 0.05rem 0 0 #00567a inset; - box-shadow: 0 0.05rem 0 0 #00567a inset; - -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-blue-bg-color, #00567a) - inset; - box-shadow: 0 0.05rem 0 0 var(--clr-label-blue-bg-color, #00567a) inset; - -webkit-transform: translateY(0.5px); - transform: translateY(0.5px); + -webkit-box-shadow: 0 0.05rem 0 0 #00567a inset; + box-shadow: 0 0.05rem 0 0 #00567a inset; + -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-blue-bg-color, #00567a) + inset; + box-shadow: 0 0.05rem 0 0 var(--clr-label-blue-bg-color, #00567a) inset; + -webkit-transform: translateY(0.5px); + transform: translateY(0.5px); } .label.label-blue > .badge, a.label.label-blue > .badge { - background: #00567a; - background: var(--clr-badge-blue-bg-color, #00567a); - color: #fff; - color: var(--clr-badge-blue-color, #fff); + background: #00567a; + background: var(--clr-badge-blue-bg-color, #00567a); + color: #fff; + color: var(--clr-badge-blue-color, #fff); } .label.label-4, .label.label-orange, a.label.label-4, a.label.label-orange { - border: 0.05rem solid; - border-color: #613200; - border-color: var(--clr-label-orange-bg-color, #613200); + border: 0.05rem solid; + border-color: #613200; + border-color: var(--clr-label-orange-bg-color, #613200); } .label.clickable.label-orange:active, .label.clickable.label-orange:hover, a.label.clickable.label-orange:active, a.label.clickable.label-orange:hover { - text-decoration: none; - background: #e8e8e8; - background: var(--clr-label-bg-hover-color, #e8e8e8); + text-decoration: none; + background: #e8e8e8; + background: var(--clr-label-bg-hover-color, #e8e8e8); } .label.clickable.label-orange:active, a.label.clickable.label-orange:active { - -webkit-box-shadow: 0 0.05rem 0 0 #613200 inset; - box-shadow: 0 0.05rem 0 0 #613200 inset; - -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-orange-bg-color, #613200) - inset; - box-shadow: 0 0.05rem 0 0 var(--clr-label-orange-bg-color, #613200) inset; - -webkit-transform: translateY(0.5px); - transform: translateY(0.5px); + -webkit-box-shadow: 0 0.05rem 0 0 #613200 inset; + box-shadow: 0 0.05rem 0 0 #613200 inset; + -webkit-box-shadow: 0 0.05rem 0 0 var(--clr-label-orange-bg-color, #613200) + inset; + box-shadow: 0 0.05rem 0 0 var(--clr-label-orange-bg-color, #613200) inset; + -webkit-transform: translateY(0.5px); + transform: translateY(0.5px); } .label.label-orange > .badge, a.label.label-orange > .badge { - background: #613200; - background: var(--clr-badge-orange-bg-color, #613200); - color: #fff; - color: var(--clr-badge-orange-color, #fff); + background: #613200; + background: var(--clr-badge-orange-bg-color, #613200); + color: #fff; + color: var(--clr-badge-orange-color, #fff); } .label.label-5, .label.label-light-blue, a.label.label-5, a.label.label-light-blue { - border: 0.05rem solid; - border-color: #179bd3; - border-color: var(--clr-label-light-blue-bg-color, #179bd3); + border: 0.05rem solid; + border-color: #179bd3; + border-color: var(--clr-label-light-blue-bg-color, #179bd3); } .label.clickable.label-light-blue:active, .label.clickable.label-light-blue:hover, a.label.clickable.label-light-blue:active, a.label.clickable.label-light-blue:hover { - text-decoration: none; - background: #e8e8e8; - background: var(--clr-label-bg-hover-color, #e8e8e8); + text-decoration: none; + background: #e8e8e8; + background: var(--clr-label-bg-hover-color, #e8e8e8); } .label.clickable.label-light-blue:active, a.label.clickable.label-light-blue:active { - -webkit-box-shadow: 0 0.05rem 0 0 #179bd3 inset; - box-shadow: 0 0.05rem 0 0 #179bd3 inset; - -webkit-box-shadow: 0 0.05rem 0 0 - var(--clr-label-light-blue-bg-color, #179bd3) inset; - box-shadow: 0 0.05rem 0 0 var(--clr-label-light-blue-bg-color, #179bd3) - inset; - -webkit-transform: translateY(0.5px); - transform: translateY(0.5px); + -webkit-box-shadow: 0 0.05rem 0 0 #179bd3 inset; + box-shadow: 0 0.05rem 0 0 #179bd3 inset; + -webkit-box-shadow: 0 0.05rem 0 0 + var(--clr-label-light-blue-bg-color, #179bd3) inset; + box-shadow: 0 0.05rem 0 0 var(--clr-label-light-blue-bg-color, #179bd3) inset; + -webkit-transform: translateY(0.5px); + transform: translateY(0.5px); } .label.label-light-blue > .badge, a.label.label-light-blue > .badge { - background: #179bd3; - background: var(--clr-badge-light-blue-bg-color, #179bd3); - color: #fff; - color: var(--clr-badge-light-blue-color, #fff); + background: #179bd3; + background: var(--clr-badge-light-blue-bg-color, #179bd3); + color: #fff; + color: var(--clr-badge-light-blue-color, #fff); } .label.label-info, a.label.label-info { - background: #e3f5fc; - background: var(--clr-label-info-bg-color, #e3f5fc); - color: #00567a; - color: var(--clr-label-info-font-color, #00567a); - border: 0.05rem solid; - border-color: #00567a; - border-color: var(--clr-label-info-border-color, #00567a); + background: #e3f5fc; + background: var(--clr-label-info-bg-color, #e3f5fc); + color: #00567a; + color: var(--clr-label-info-font-color, #00567a); + border: 0.05rem solid; + border-color: #00567a; + border-color: var(--clr-label-info-border-color, #00567a); } .label.label-success, a.label.label-success { - background: #dff0d0; - background: var(--clr-label-success-bg-color, #dff0d0); - color: #306b00; - color: var(--clr-label-success-font-color, #306b00); - border: 0.05rem solid; - border-color: #306b00; - border-color: var(--clr-label-success-border-color, #306b00); + background: #dff0d0; + background: var(--clr-label-success-bg-color, #dff0d0); + color: #306b00; + color: var(--clr-label-success-font-color, #306b00); + border: 0.05rem solid; + border-color: #306b00; + border-color: var(--clr-label-success-border-color, #306b00); } .label.label-warning, a.label.label-warning { - background: #fff4c7; - background: var(--clr-label-warning-bg-color, #fff4c7); - color: #333; - color: var(--clr-label-warning-font-color, #333); - border: 0.05rem solid; - border-color: #ad7600; - border-color: var(--clr-label-warning-border-color, #ad7600); + background: #fff4c7; + background: var(--clr-label-warning-bg-color, #fff4c7); + color: #333; + color: var(--clr-label-warning-font-color, #333); + border: 0.05rem solid; + border-color: #ad7600; + border-color: var(--clr-label-warning-border-color, #ad7600); } .label.label-danger, a.label.label-danger { - background: #feddd7; - background: var(--clr-label-danger-bg-color, #feddd7); - color: #991700; - color: var(--clr-label-danger-font-color, #991700); - border: 0.05rem solid; - border-color: #991700; - border-color: var(--clr-label-danger-border-color, #991700); + background: #feddd7; + background: var(--clr-label-danger-bg-color, #feddd7); + color: #991700; + color: var(--clr-label-danger-font-color, #991700); + border: 0.05rem solid; + border-color: #991700; + border-color: var(--clr-label-danger-border-color, #991700); } .label > .badge, a.label > .badge { - margin: 0 -0.45rem 0 0.3rem; + margin: 0 -0.45rem 0 0.3rem; } @-moz-document url-prefix() { - .label, - a.label { - vertical-align: bottom; - } + .label, + a.label { + vertical-align: bottom; + } } :root { - --clr-login-title-color: var(--clr-h1-color); - --clr-login-title-font-weight: var(--clr-h1-font-weight); - --clr-login-title-font-family: var(--clr-h1-font-family); - --clr-login-trademark-color: var(--clr-h2-color); - --clr-login-trademark-font-weight: var(--clr-h2-font-weight); - --clr-login-trademark-font-family: var(--clr-h2-font-family); - --clr-login-subtitle-color: var(--clr-h3-color); - --clr-login-subtitle-font-weight: var(--clr-h3-font-weight); - --clr-login-subtitle-font-family: var(--clr-h3-font-family); - --clr-login-background-color: var(--clr-global-app-background); - --clr-login-background: url('data:image/svg+xml;charset=utf8,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C%21DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%0D%0A%20%20%20%20%20version%3D%221.1%22%0D%0A%20%20%20%20%20id%3D%22no-aspect-ratio%22%0D%0A%20%20%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0D%0A%20%20%20%20%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%0D%0A%20%20%20%20%20x%3D%220px%22%0D%0A%20%20%20%20%20y%3D%220px%22%0D%0A%20%20%20%20%20height%3D%222055.55px%22%0D%0A%20%20%20%20%20width%3D%221440px%22%0D%0A%20%20%20%20%20viewBox%3D%220%200%202055.55%201440%22%0D%0A%20%20%20%20%20preserveAspectRatio%3D%22xMinYMin%20slice%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%3Cdesc%3ELogin%20Image%3C%2Fdesc%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20transform%3D%22translate%280.000000%2C%20-4.000000%29%22%3E%0D%0A%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23FAFAFA%22%20x%3D%220%22%20y%3D%224%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221108.43%201443.63%201109.08%201443.63%20443.44%20777.74%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%220.79%20334.92%20443.44%20777.74%200.79%20334.49%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%220.79%20211.88%200.79%20329.6%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22160.65%20169.74%200.79%209.73%200.79%20211.88%2090.27%20301.46%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22503.77%201443.63%20697.47%201443.63%20803.74%201337.36%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22158.33%20691.15%200.79%20848.72%200.79%201427.43%20447.52%20980.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CEDDE0%22%20points%3D%22257.71%20591.75%200.79%20334.49%200.79%20533.42%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A9C9D5%22%20points%3D%220.79%20533.42%200.79%20848.72%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22806.46%201140.89%20546.94%20881.28%20447.52%20980.7%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238FC4DF%22%20points%3D%22447.52%20980.7%200.79%201427.43%200.79%201443.63%20503.77%201443.63%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%22608.23%20819.99%20546.94%20881.28%20806.46%201140.89%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22420.05%20429.39%20319.01%20530.45%20608.23%20819.99%20709.3%20718.91%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22709.3%20718.91%20608.23%20819.99%20867.64%201079.7%20968.74%20978.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22619.59%20229.82%20393.42%203.12%20327.27%203.12%20160.65%20169.74%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22319.01%20530.45%20319.01%20530.45%2090.27%20301.46%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22160.65%20169.74%2059.62%20270.77%2090.27%20301.46%20319.01%20530.45%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2384C4D2%22%20points%3D%2259.62%20270.77%200.79%20329.6%200.79%20334.49%20257.71%20591.75%20319.01%20530.45%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22537.55%203.12%20393.42%203.12%20619.59%20229.82%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2387D1DB%22%20points%3D%22846.25%203.12%20537.55%203.12%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22909.87%201443.63%20850.19%201383.87%20790.43%201443.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22319.01%20530.45%20257.71%20591.75%20443.44%20777.74%20546.94%20881.28%20608.23%20819.99%20867.64%201079.7%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22867.64%201079.7%20806.46%201140.89%20903.31%201237.78%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221065.57%201075.52%20968.74%20978.6%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22964.46%201176.63%20867.64%201079.7%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201231.16%201443.63%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221240.08%20707.22%201167.9%20779.4%201264.68%20876.4%201336.87%20804.22%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22980.83%20447.39%20691.74%20157.66%20619.59%20229.82%20908.66%20519.56%20980.83%20447.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22709.3%20718.91%20968.74%20978.6%201167.91%20779.4%20908.66%20519.55%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22980.83%20447.39%20908.66%20519.55%201167.91%20779.4%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221034.59%203.12%20846.25%203.12%20691.74%20157.66%20980.83%20447.39%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221240.08%20707.21%201336.87%20804.22%201586.01%20555.08%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%221229.75%20198.47%20980.83%20447.39%201240.08%20707.21%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221292.22%201302.38%201433.32%201443.63%201830.61%201443.63%201491.18%201103.42%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221010.92%201223.13%20949.78%201284.27%201109.08%201443.63%201150.98%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2375B8C5%22%20points%3D%221150.98%201443.63%201231.16%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221292.22%201302.38%201112.03%201122.02%201010.92%201223.13%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221191.09%201403.51%201231.16%201443.63%201433.32%201443.63%201292.22%201302.38%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221383.3%20850.75%201311.12%20922.94%201491.18%201103.42%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221812.65%20781.95%201632.46%20601.59%201383.3%20850.75%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2378CAD4%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%22803.74%201337.36%20850.19%201383.87%20949.78%201284.27%20903.31%201237.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221065.57%201075.52%201112.03%201122.02%201311.12%20922.94%201264.69%20876.4%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2377B8D9%22%20points%3D%22697.47%201443.63%20790.43%201443.63%20850.19%201383.87%20803.74%201337.36%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2396C7DF%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2357A8D0%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23000000%22%20opacity%3D%220.42%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2357A8D0%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD3E6%22%20points%3D%222056%200.12%201645.49%200.12%201648.49%203.12%201944.07%203.12%201796.22%20150.99%201893.12%20247.97%202054.45%2086.64%202054.45%20179.6%201939.58%20294.47%202056%20411%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237AB9D9%22%20points%3D%221648.49%203.12%201796.22%20150.99%201944.07%203.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2366AED4%22%20points%3D%222054.45%2086.64%201893.12%20247.97%201939.58%20294.47%202054.45%20179.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221884.82%20709.78%202054.45%20879.57%202054.45%20540.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221489.14%20458.12%201489.14%20458.12%201371.13%20339.99%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221796.22%20150.99%201648.49%203.12%201425.1%203.12%201301.91%20126.31%201561.3%20385.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2391C5E0%22%20transform%3D%22translate%281798.954066%2C%20388.798781%29%20rotate%28-44.970000%29%20translate%28-1798.954066%2C%20-388.798781%29%20%22%20x%3D%221632.82407%22%20y%3D%22355.933781%22%20width%3D%22332.26%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221586.01%20555.08%201632.46%20601.59%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281573.711577%2C%20470.620263%29%20rotate%28-45.000000%29%20translate%28-1573.711577%2C%20-470.620263%29%20%22%20x%3D%221522.68158%22%20y%3D%22402.085263%22%20width%3D%22102.06%22%20height%3D%22137.07%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281758.676758%2C%20655.767120%29%20rotate%28-44.970000%29%20translate%28-1758.676758%2C%20-655.767120%29%20%22%20x%3D%221707.64676%22%20y%3D%22528.29212%22%20width%3D%22102.06%22%20height%3D%22254.95%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B3EAEE%22%20points%3D%221301.91%20126.31%201178.84%203.12%201034.59%203.12%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2383C0C8%22%20points%3D%221812.65%20781.95%202054.45%201023.99%202054.45%20879.57%201884.82%20709.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%237DC6DC%22%20transform%3D%22translate%281395.516901%2C%20292.206519%29%20rotate%28-45.000000%29%20translate%28-1395.516901%2C%20-292.206519%29%20%22%20x%3D%221344.4919%22%20y%3D%22108.701519%22%20width%3D%22102.05%22%20height%3D%22367.01%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2368B8D5%22%20transform%3D%22translate%281645.313619%2C%20542.249760%29%20rotate%28-45.000000%29%20translate%28-1645.313619%2C%20-542.249760%29%20%22%20x%3D%221594.28362%22%20y%3D%22509.38476%22%20width%3D%22102.06%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20transform%3D%22translate%280.000000%2C%203.000000%29%22%20stroke%3D%22%23000000%22%20opacity%3D%220.15%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M0.95%2C0.12%20L0.95%2C840.12%22%20id%3D%22Shape%22%3E%3C%2Fpath%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E'); - --clr-login-error-background-color: var(--clr-color-danger-800); - --clr-login-error-border-radius: var(--clr-global-borderradius); - --clr-login-panel-line-color: var(--clr-color-neutral-1000); - --clr-login-panel-line-opacity: 0.1; + --clr-login-title-color: var(--clr-h1-color); + --clr-login-title-font-weight: var(--clr-h1-font-weight); + --clr-login-title-font-family: var(--clr-h1-font-family); + --clr-login-trademark-color: var(--clr-h2-color); + --clr-login-trademark-font-weight: var(--clr-h2-font-weight); + --clr-login-trademark-font-family: var(--clr-h2-font-family); + --clr-login-subtitle-color: var(--clr-h3-color); + --clr-login-subtitle-font-weight: var(--clr-h3-font-weight); + --clr-login-subtitle-font-family: var(--clr-h3-font-family); + --clr-login-background-color: var(--clr-global-app-background); + --clr-login-background: url("data:image/svg+xml;charset=utf8,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C%21DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%0D%0A%20%20%20%20%20version%3D%221.1%22%0D%0A%20%20%20%20%20id%3D%22no-aspect-ratio%22%0D%0A%20%20%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0D%0A%20%20%20%20%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%0D%0A%20%20%20%20%20x%3D%220px%22%0D%0A%20%20%20%20%20y%3D%220px%22%0D%0A%20%20%20%20%20height%3D%222055.55px%22%0D%0A%20%20%20%20%20width%3D%221440px%22%0D%0A%20%20%20%20%20viewBox%3D%220%200%202055.55%201440%22%0D%0A%20%20%20%20%20preserveAspectRatio%3D%22xMinYMin%20slice%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%3Cdesc%3ELogin%20Image%3C%2Fdesc%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20transform%3D%22translate%280.000000%2C%20-4.000000%29%22%3E%0D%0A%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23FAFAFA%22%20x%3D%220%22%20y%3D%224%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221108.43%201443.63%201109.08%201443.63%20443.44%20777.74%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%220.79%20334.92%20443.44%20777.74%200.79%20334.49%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%220.79%20211.88%200.79%20329.6%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22160.65%20169.74%200.79%209.73%200.79%20211.88%2090.27%20301.46%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22503.77%201443.63%20697.47%201443.63%20803.74%201337.36%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22158.33%20691.15%200.79%20848.72%200.79%201427.43%20447.52%20980.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CEDDE0%22%20points%3D%22257.71%20591.75%200.79%20334.49%200.79%20533.42%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A9C9D5%22%20points%3D%220.79%20533.42%200.79%20848.72%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22806.46%201140.89%20546.94%20881.28%20447.52%20980.7%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238FC4DF%22%20points%3D%22447.52%20980.7%200.79%201427.43%200.79%201443.63%20503.77%201443.63%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%22608.23%20819.99%20546.94%20881.28%20806.46%201140.89%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22420.05%20429.39%20319.01%20530.45%20608.23%20819.99%20709.3%20718.91%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22709.3%20718.91%20608.23%20819.99%20867.64%201079.7%20968.74%20978.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22619.59%20229.82%20393.42%203.12%20327.27%203.12%20160.65%20169.74%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22319.01%20530.45%20319.01%20530.45%2090.27%20301.46%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22160.65%20169.74%2059.62%20270.77%2090.27%20301.46%20319.01%20530.45%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2384C4D2%22%20points%3D%2259.62%20270.77%200.79%20329.6%200.79%20334.49%20257.71%20591.75%20319.01%20530.45%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22537.55%203.12%20393.42%203.12%20619.59%20229.82%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2387D1DB%22%20points%3D%22846.25%203.12%20537.55%203.12%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22909.87%201443.63%20850.19%201383.87%20790.43%201443.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22319.01%20530.45%20257.71%20591.75%20443.44%20777.74%20546.94%20881.28%20608.23%20819.99%20867.64%201079.7%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22867.64%201079.7%20806.46%201140.89%20903.31%201237.78%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221065.57%201075.52%20968.74%20978.6%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22964.46%201176.63%20867.64%201079.7%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201231.16%201443.63%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221240.08%20707.22%201167.9%20779.4%201264.68%20876.4%201336.87%20804.22%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22980.83%20447.39%20691.74%20157.66%20619.59%20229.82%20908.66%20519.56%20980.83%20447.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22709.3%20718.91%20968.74%20978.6%201167.91%20779.4%20908.66%20519.55%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22980.83%20447.39%20908.66%20519.55%201167.91%20779.4%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221034.59%203.12%20846.25%203.12%20691.74%20157.66%20980.83%20447.39%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221240.08%20707.21%201336.87%20804.22%201586.01%20555.08%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%221229.75%20198.47%20980.83%20447.39%201240.08%20707.21%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221292.22%201302.38%201433.32%201443.63%201830.61%201443.63%201491.18%201103.42%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221010.92%201223.13%20949.78%201284.27%201109.08%201443.63%201150.98%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2375B8C5%22%20points%3D%221150.98%201443.63%201231.16%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221292.22%201302.38%201112.03%201122.02%201010.92%201223.13%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221191.09%201403.51%201231.16%201443.63%201433.32%201443.63%201292.22%201302.38%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221383.3%20850.75%201311.12%20922.94%201491.18%201103.42%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221812.65%20781.95%201632.46%20601.59%201383.3%20850.75%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2378CAD4%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%22803.74%201337.36%20850.19%201383.87%20949.78%201284.27%20903.31%201237.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221065.57%201075.52%201112.03%201122.02%201311.12%20922.94%201264.69%20876.4%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2377B8D9%22%20points%3D%22697.47%201443.63%20790.43%201443.63%20850.19%201383.87%20803.74%201337.36%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2396C7DF%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2357A8D0%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23000000%22%20opacity%3D%220.42%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2357A8D0%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD3E6%22%20points%3D%222056%200.12%201645.49%200.12%201648.49%203.12%201944.07%203.12%201796.22%20150.99%201893.12%20247.97%202054.45%2086.64%202054.45%20179.6%201939.58%20294.47%202056%20411%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237AB9D9%22%20points%3D%221648.49%203.12%201796.22%20150.99%201944.07%203.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2366AED4%22%20points%3D%222054.45%2086.64%201893.12%20247.97%201939.58%20294.47%202054.45%20179.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221884.82%20709.78%202054.45%20879.57%202054.45%20540.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221489.14%20458.12%201489.14%20458.12%201371.13%20339.99%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221796.22%20150.99%201648.49%203.12%201425.1%203.12%201301.91%20126.31%201561.3%20385.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2391C5E0%22%20transform%3D%22translate%281798.954066%2C%20388.798781%29%20rotate%28-44.970000%29%20translate%28-1798.954066%2C%20-388.798781%29%20%22%20x%3D%221632.82407%22%20y%3D%22355.933781%22%20width%3D%22332.26%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221586.01%20555.08%201632.46%20601.59%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281573.711577%2C%20470.620263%29%20rotate%28-45.000000%29%20translate%28-1573.711577%2C%20-470.620263%29%20%22%20x%3D%221522.68158%22%20y%3D%22402.085263%22%20width%3D%22102.06%22%20height%3D%22137.07%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281758.676758%2C%20655.767120%29%20rotate%28-44.970000%29%20translate%28-1758.676758%2C%20-655.767120%29%20%22%20x%3D%221707.64676%22%20y%3D%22528.29212%22%20width%3D%22102.06%22%20height%3D%22254.95%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B3EAEE%22%20points%3D%221301.91%20126.31%201178.84%203.12%201034.59%203.12%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2383C0C8%22%20points%3D%221812.65%20781.95%202054.45%201023.99%202054.45%20879.57%201884.82%20709.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%237DC6DC%22%20transform%3D%22translate%281395.516901%2C%20292.206519%29%20rotate%28-45.000000%29%20translate%28-1395.516901%2C%20-292.206519%29%20%22%20x%3D%221344.4919%22%20y%3D%22108.701519%22%20width%3D%22102.05%22%20height%3D%22367.01%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2368B8D5%22%20transform%3D%22translate%281645.313619%2C%20542.249760%29%20rotate%28-45.000000%29%20translate%28-1645.313619%2C%20-542.249760%29%20%22%20x%3D%221594.28362%22%20y%3D%22509.38476%22%20width%3D%22102.06%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20transform%3D%22translate%280.000000%2C%203.000000%29%22%20stroke%3D%22%23000000%22%20opacity%3D%220.15%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M0.95%2C0.12%20L0.95%2C840.12%22%20id%3D%22Shape%22%3E%3C%2Fpath%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E"); + --clr-login-error-background-color: var(--clr-color-danger-800); + --clr-login-error-border-radius: var(--clr-global-borderradius); + --clr-login-panel-line-color: var(--clr-color-neutral-1000); + --clr-login-panel-line-opacity: 0.1; } .login-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - height: 100%; - background: url('data:image/svg+xml;charset=utf8,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C%21DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%0D%0A%20%20%20%20%20version%3D%221.1%22%0D%0A%20%20%20%20%20id%3D%22no-aspect-ratio%22%0D%0A%20%20%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0D%0A%20%20%20%20%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%0D%0A%20%20%20%20%20x%3D%220px%22%0D%0A%20%20%20%20%20y%3D%220px%22%0D%0A%20%20%20%20%20height%3D%222055.55px%22%0D%0A%20%20%20%20%20width%3D%221440px%22%0D%0A%20%20%20%20%20viewBox%3D%220%200%202055.55%201440%22%0D%0A%20%20%20%20%20preserveAspectRatio%3D%22xMinYMin%20slice%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%3Cdesc%3ELogin%20Image%3C%2Fdesc%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20transform%3D%22translate%280.000000%2C%20-4.000000%29%22%3E%0D%0A%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23FAFAFA%22%20x%3D%220%22%20y%3D%224%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221108.43%201443.63%201109.08%201443.63%20443.44%20777.74%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%220.79%20334.92%20443.44%20777.74%200.79%20334.49%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%220.79%20211.88%200.79%20329.6%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22160.65%20169.74%200.79%209.73%200.79%20211.88%2090.27%20301.46%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22503.77%201443.63%20697.47%201443.63%20803.74%201337.36%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22158.33%20691.15%200.79%20848.72%200.79%201427.43%20447.52%20980.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CEDDE0%22%20points%3D%22257.71%20591.75%200.79%20334.49%200.79%20533.42%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A9C9D5%22%20points%3D%220.79%20533.42%200.79%20848.72%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22806.46%201140.89%20546.94%20881.28%20447.52%20980.7%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238FC4DF%22%20points%3D%22447.52%20980.7%200.79%201427.43%200.79%201443.63%20503.77%201443.63%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%22608.23%20819.99%20546.94%20881.28%20806.46%201140.89%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22420.05%20429.39%20319.01%20530.45%20608.23%20819.99%20709.3%20718.91%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22709.3%20718.91%20608.23%20819.99%20867.64%201079.7%20968.74%20978.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22619.59%20229.82%20393.42%203.12%20327.27%203.12%20160.65%20169.74%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22319.01%20530.45%20319.01%20530.45%2090.27%20301.46%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22160.65%20169.74%2059.62%20270.77%2090.27%20301.46%20319.01%20530.45%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2384C4D2%22%20points%3D%2259.62%20270.77%200.79%20329.6%200.79%20334.49%20257.71%20591.75%20319.01%20530.45%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22537.55%203.12%20393.42%203.12%20619.59%20229.82%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2387D1DB%22%20points%3D%22846.25%203.12%20537.55%203.12%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22909.87%201443.63%20850.19%201383.87%20790.43%201443.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22319.01%20530.45%20257.71%20591.75%20443.44%20777.74%20546.94%20881.28%20608.23%20819.99%20867.64%201079.7%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22867.64%201079.7%20806.46%201140.89%20903.31%201237.78%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221065.57%201075.52%20968.74%20978.6%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22964.46%201176.63%20867.64%201079.7%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201231.16%201443.63%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221240.08%20707.22%201167.9%20779.4%201264.68%20876.4%201336.87%20804.22%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22980.83%20447.39%20691.74%20157.66%20619.59%20229.82%20908.66%20519.56%20980.83%20447.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22709.3%20718.91%20968.74%20978.6%201167.91%20779.4%20908.66%20519.55%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22980.83%20447.39%20908.66%20519.55%201167.91%20779.4%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221034.59%203.12%20846.25%203.12%20691.74%20157.66%20980.83%20447.39%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221240.08%20707.21%201336.87%20804.22%201586.01%20555.08%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%221229.75%20198.47%20980.83%20447.39%201240.08%20707.21%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221292.22%201302.38%201433.32%201443.63%201830.61%201443.63%201491.18%201103.42%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221010.92%201223.13%20949.78%201284.27%201109.08%201443.63%201150.98%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2375B8C5%22%20points%3D%221150.98%201443.63%201231.16%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221292.22%201302.38%201112.03%201122.02%201010.92%201223.13%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221191.09%201403.51%201231.16%201443.63%201433.32%201443.63%201292.22%201302.38%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221383.3%20850.75%201311.12%20922.94%201491.18%201103.42%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221812.65%20781.95%201632.46%20601.59%201383.3%20850.75%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2378CAD4%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%22803.74%201337.36%20850.19%201383.87%20949.78%201284.27%20903.31%201237.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221065.57%201075.52%201112.03%201122.02%201311.12%20922.94%201264.69%20876.4%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2377B8D9%22%20points%3D%22697.47%201443.63%20790.43%201443.63%20850.19%201383.87%20803.74%201337.36%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2396C7DF%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2357A8D0%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23000000%22%20opacity%3D%220.42%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2357A8D0%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD3E6%22%20points%3D%222056%200.12%201645.49%200.12%201648.49%203.12%201944.07%203.12%201796.22%20150.99%201893.12%20247.97%202054.45%2086.64%202054.45%20179.6%201939.58%20294.47%202056%20411%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237AB9D9%22%20points%3D%221648.49%203.12%201796.22%20150.99%201944.07%203.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2366AED4%22%20points%3D%222054.45%2086.64%201893.12%20247.97%201939.58%20294.47%202054.45%20179.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221884.82%20709.78%202054.45%20879.57%202054.45%20540.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221489.14%20458.12%201489.14%20458.12%201371.13%20339.99%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221796.22%20150.99%201648.49%203.12%201425.1%203.12%201301.91%20126.31%201561.3%20385.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2391C5E0%22%20transform%3D%22translate%281798.954066%2C%20388.798781%29%20rotate%28-44.970000%29%20translate%28-1798.954066%2C%20-388.798781%29%20%22%20x%3D%221632.82407%22%20y%3D%22355.933781%22%20width%3D%22332.26%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221586.01%20555.08%201632.46%20601.59%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281573.711577%2C%20470.620263%29%20rotate%28-45.000000%29%20translate%28-1573.711577%2C%20-470.620263%29%20%22%20x%3D%221522.68158%22%20y%3D%22402.085263%22%20width%3D%22102.06%22%20height%3D%22137.07%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281758.676758%2C%20655.767120%29%20rotate%28-44.970000%29%20translate%28-1758.676758%2C%20-655.767120%29%20%22%20x%3D%221707.64676%22%20y%3D%22528.29212%22%20width%3D%22102.06%22%20height%3D%22254.95%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B3EAEE%22%20points%3D%221301.91%20126.31%201178.84%203.12%201034.59%203.12%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2383C0C8%22%20points%3D%221812.65%20781.95%202054.45%201023.99%202054.45%20879.57%201884.82%20709.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%237DC6DC%22%20transform%3D%22translate%281395.516901%2C%20292.206519%29%20rotate%28-45.000000%29%20translate%28-1395.516901%2C%20-292.206519%29%20%22%20x%3D%221344.4919%22%20y%3D%22108.701519%22%20width%3D%22102.05%22%20height%3D%22367.01%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2368B8D5%22%20transform%3D%22translate%281645.313619%2C%20542.249760%29%20rotate%28-45.000000%29%20translate%28-1645.313619%2C%20-542.249760%29%20%22%20x%3D%221594.28362%22%20y%3D%22509.38476%22%20width%3D%22102.06%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20transform%3D%22translate%280.000000%2C%203.000000%29%22%20stroke%3D%22%23000000%22%20opacity%3D%220.15%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M0.95%2C0.12%20L0.95%2C840.12%22%20id%3D%22Shape%22%3E%3C%2Fpath%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E'); - background-size: 100%; - background-position: 25.2rem 0; - background-repeat: no-repeat; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + height: 100%; + background: url("data:image/svg+xml;charset=utf8,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0D%0A%3C%21DOCTYPE%20svg%20PUBLIC%20%22-%2F%2FW3C%2F%2FDTD%20SVG%201.1%2F%2FEN%22%20%22http%3A%2F%2Fwww.w3.org%2FGraphics%2FSVG%2F1.1%2FDTD%2Fsvg11.dtd%22%3E%0D%0A%3Csvg%0D%0A%20%20%20%20%20version%3D%221.1%22%0D%0A%20%20%20%20%20id%3D%22no-aspect-ratio%22%0D%0A%20%20%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0D%0A%20%20%20%20%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%0D%0A%20%20%20%20%20x%3D%220px%22%0D%0A%20%20%20%20%20y%3D%220px%22%0D%0A%20%20%20%20%20height%3D%222055.55px%22%0D%0A%20%20%20%20%20width%3D%221440px%22%0D%0A%20%20%20%20%20viewBox%3D%220%200%202055.55%201440%22%0D%0A%20%20%20%20%20preserveAspectRatio%3D%22xMinYMin%20slice%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%3Cdesc%3ELogin%20Image%3C%2Fdesc%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20transform%3D%22translate%280.000000%2C%20-4.000000%29%22%3E%0D%0A%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23FAFAFA%22%20x%3D%220%22%20y%3D%224%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221108.43%201443.63%201109.08%201443.63%20443.44%20777.74%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%220.79%20334.92%20443.44%20777.74%200.79%20334.49%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%220.79%20211.88%200.79%20329.6%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22160.65%20169.74%200.79%209.73%200.79%20211.88%2090.27%20301.46%2059.62%20270.77%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22503.77%201443.63%20697.47%201443.63%20803.74%201337.36%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22158.33%20691.15%200.79%20848.72%200.79%201427.43%20447.52%20980.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CEDDE0%22%20points%3D%22257.71%20591.75%200.79%20334.49%200.79%20533.42%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A9C9D5%22%20points%3D%220.79%20533.42%200.79%20848.72%20158.33%20691.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22806.46%201140.89%20546.94%20881.28%20447.52%20980.7%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238FC4DF%22%20points%3D%22447.52%20980.7%200.79%201427.43%200.79%201443.63%20503.77%201443.63%20706.93%201240.43%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%22608.23%20819.99%20546.94%20881.28%20806.46%201140.89%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22420.05%20429.39%20319.01%20530.45%20608.23%20819.99%20709.3%20718.91%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22709.3%20718.91%20608.23%20819.99%20867.64%201079.7%20968.74%20978.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22619.59%20229.82%20393.42%203.12%20327.27%203.12%20160.65%20169.74%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%238EB5BC%22%20points%3D%22319.01%20530.45%20319.01%20530.45%2090.27%20301.46%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22160.65%20169.74%2059.62%20270.77%2090.27%20301.46%20319.01%20530.45%20420.05%20429.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2384C4D2%22%20points%3D%2259.62%20270.77%200.79%20329.6%200.79%20334.49%20257.71%20591.75%20319.01%20530.45%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237CB0C7%22%20points%3D%22537.55%203.12%20393.42%203.12%20619.59%20229.82%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2387D1DB%22%20points%3D%22846.25%203.12%20537.55%203.12%20691.74%20157.66%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23CDE3EE%22%20points%3D%22909.87%201443.63%20850.19%201383.87%20790.43%201443.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22319.01%20530.45%20257.71%20591.75%20443.44%20777.74%20546.94%20881.28%20608.23%20819.99%20867.64%201079.7%20867.64%201079.7%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22867.64%201079.7%20806.46%201140.89%20903.31%201237.78%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221065.57%201075.52%20968.74%20978.6%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22964.46%201176.63%20867.64%201079.7%20867.64%201079.7%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201231.16%201443.63%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221240.08%20707.22%201167.9%20779.4%201264.68%20876.4%201336.87%20804.22%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%22980.83%20447.39%20691.74%20157.66%20619.59%20229.82%20908.66%20519.56%20980.83%20447.39%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%22709.3%20718.91%20968.74%20978.6%201167.91%20779.4%20908.66%20519.55%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2369AFD4%22%20points%3D%22980.83%20447.39%20908.66%20519.55%201167.91%20779.4%201240.08%20707.21%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221034.59%203.12%20846.25%203.12%20691.74%20157.66%20980.83%20447.39%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221240.08%20707.21%201336.87%20804.22%201586.01%20555.08%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2370C0DC%22%20points%3D%221229.75%20198.47%20980.83%20447.39%201240.08%20707.21%201489.14%20458.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221292.22%201302.38%201433.32%201443.63%201830.61%201443.63%201491.18%201103.42%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221010.92%201223.13%20949.78%201284.27%201109.08%201443.63%201150.98%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2375B8C5%22%20points%3D%221150.98%201443.63%201231.16%201443.63%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221292.22%201302.38%201112.03%201122.02%201010.92%201223.13%201191.09%201403.51%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221191.09%201403.51%201231.16%201443.63%201433.32%201443.63%201292.22%201302.38%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221383.3%20850.75%201311.12%20922.94%201491.18%201103.42%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%236EA4BC%22%20points%3D%221491.18%201103.42%201830.61%201443.63%201974.86%201443.63%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%221812.65%20781.95%201632.46%20601.59%201383.3%20850.75%201563.37%201031.23%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2378CAD4%22%20points%3D%221563.37%201031.23%201974.86%201443.63%202054.45%201443.63%202054.45%201023.99%201812.65%20781.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%22803.74%201337.36%20850.19%201383.87%20949.78%201284.27%20903.31%201237.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221065.57%201075.52%201112.03%201122.02%201311.12%20922.94%201264.69%20876.4%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2377B8D9%22%20points%3D%22697.47%201443.63%20790.43%201443.63%20850.19%201383.87%20803.74%201337.36%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23A0DEEA%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%22964.46%201176.63%20903.31%201237.78%20949.78%201284.27%201010.92%201223.13%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2396C7DF%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2357A8D0%22%20transform%3D%22translate%281038.247297%2C%201149.275429%29%20rotate%28-44.970000%29%20translate%28-1038.247297%2C%20-1149.275429%29%20%22%20x%3D%22966.752297%22%20y%3D%221116.41043%22%20width%3D%22142.99%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2396C7DF%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23000000%22%20opacity%3D%220.42%22%20points%3D%221010.92%201223.13%201010.92%201223.13%20964.46%201176.63%20964.46%201176.63%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23007CBB%22%20opacity%3D%220.4%22%20style%3D%22mix-blend-mode%3A%20multiply%3B%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2357A8D0%22%20points%3D%221336.87%20804.22%201264.69%20876.4%201311.12%20922.94%201383.3%20850.75%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2393D8CA%22%20opacity%3D%220.6%22%20style%3D%22mix-blend-mode%3A%20overlay%3B%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%235DB5D6%22%20points%3D%221336.87%20804.22%201383.3%20850.75%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD3E6%22%20points%3D%222056%200.12%201645.49%200.12%201648.49%203.12%201944.07%203.12%201796.22%20150.99%201893.12%20247.97%202054.45%2086.64%202054.45%20179.6%201939.58%20294.47%202056%20411%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%237AB9D9%22%20points%3D%221648.49%203.12%201796.22%20150.99%201944.07%203.12%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2366AED4%22%20points%3D%222054.45%2086.64%201893.12%20247.97%201939.58%20294.47%202054.45%20179.6%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B7CED2%22%20points%3D%221884.82%20709.78%202054.45%20879.57%202054.45%20540.15%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221489.14%20458.12%201489.14%20458.12%201371.13%20339.99%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23AFD4E7%22%20points%3D%221796.22%20150.99%201648.49%203.12%201425.1%203.12%201301.91%20126.31%201561.3%20385.95%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2391C5E0%22%20transform%3D%22translate%281798.954066%2C%20388.798781%29%20rotate%28-44.970000%29%20translate%28-1798.954066%2C%20-388.798781%29%20%22%20x%3D%221632.82407%22%20y%3D%22355.933781%22%20width%3D%22332.26%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2391C5E0%22%20points%3D%221586.01%20555.08%201632.46%20601.59%201632.46%20601.59%201586.01%20555.08%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281573.711577%2C%20470.620263%29%20rotate%28-45.000000%29%20translate%28-1573.711577%2C%20-470.620263%29%20%22%20x%3D%221522.68158%22%20y%3D%22402.085263%22%20width%3D%22102.06%22%20height%3D%22137.07%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%23B3EAEE%22%20transform%3D%22translate%281758.676758%2C%20655.767120%29%20rotate%28-44.970000%29%20translate%28-1758.676758%2C%20-655.767120%29%20%22%20x%3D%221707.64676%22%20y%3D%22528.29212%22%20width%3D%22102.06%22%20height%3D%22254.95%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%23B3EAEE%22%20points%3D%221301.91%20126.31%201178.84%203.12%201034.59%203.12%201229.75%20198.47%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpolygon%20fill%3D%22%2383C0C8%22%20points%3D%221812.65%20781.95%202054.45%201023.99%202054.45%20879.57%201884.82%20709.78%22%3E%3C%2Fpolygon%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%237DC6DC%22%20transform%3D%22translate%281395.516901%2C%20292.206519%29%20rotate%28-45.000000%29%20translate%28-1395.516901%2C%20-292.206519%29%20%22%20x%3D%221344.4919%22%20y%3D%22108.701519%22%20width%3D%22102.05%22%20height%3D%22367.01%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20fill%3D%22%2368B8D5%22%20transform%3D%22translate%281645.313619%2C%20542.249760%29%20rotate%28-45.000000%29%20translate%28-1645.313619%2C%20-542.249760%29%20%22%20x%3D%221594.28362%22%20y%3D%22509.38476%22%20width%3D%22102.06%22%20height%3D%2265.73%22%3E%3C%2Frect%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cg%20transform%3D%22translate%280.000000%2C%203.000000%29%22%20stroke%3D%22%23000000%22%20opacity%3D%220.15%22%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M0.95%2C0.12%20L0.95%2C840.12%22%20id%3D%22Shape%22%3E%3C%2Fpath%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0D%0A%20%20%20%20%3C%2Fg%3E%0D%0A%3C%2Fsvg%3E"); + background-size: 100%; + background-position: 25.2rem 0; + background-repeat: no-repeat; } .login-wrapper .login { - background: #fafafa; - background: var(--clr-login-background-color, #fafafa); - position: relative; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - padding: 1.2rem 3rem; - height: auto; - min-height: 100vh; - width: 25.2rem; + background: #fafafa; + background: var(--clr-login-background-color, #fafafa); + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + padding: 1.2rem 3rem; + height: auto; + min-height: 100vh; + width: 25.2rem; } .login-wrapper .login .title { - color: #000; - color: var(--clr-login-title-color, #000); - font-weight: 200; - font-weight: var(--clr-login-title-font-weight, 200); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-login-title-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 1.6rem; - letter-spacing: normal; - line-height: 1.8rem; + color: #000; + color: var(--clr-login-title-color, #000); + font-weight: 200; + font-weight: var(--clr-login-title-font-weight, 200); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-login-title-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 1.6rem; + letter-spacing: normal; + line-height: 1.8rem; } .login-wrapper .login .title .welcome { - line-height: 1.8rem; + line-height: 1.8rem; } .login-wrapper .login .title .hint { - color: #000; - color: var(--clr-login-title-color, #000); - margin-top: 1.5rem; - font-size: 0.7rem; + color: #000; + color: var(--clr-login-title-color, #000); + margin-top: 1.5rem; + font-size: 0.7rem; } .login-wrapper .login .trademark { - color: #000; - color: var(--clr-login-trademark-color, #000); - font-weight: 200; - font-weight: var(--clr-login-trademark-font-weight, 200); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-login-trademark-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 1.4rem; - letter-spacing: normal; + color: #000; + color: var(--clr-login-trademark-color, #000); + font-weight: 200; + font-weight: var(--clr-login-trademark-font-weight, 200); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-login-trademark-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 1.4rem; + letter-spacing: normal; } .login-wrapper .login .subtitle { - font-weight: 200; - font-weight: var(--clr-login-subtitle-font-weight, 200); - color: #000; - color: var(--clr-login-subtitle-color, #000); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-login-subtitle-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 1.1rem; - letter-spacing: normal; - line-height: 1.8rem; + font-weight: 200; + font-weight: var(--clr-login-subtitle-font-weight, 200); + color: #000; + color: var(--clr-login-subtitle-color, #000); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-login-subtitle-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 1.1rem; + letter-spacing: normal; + line-height: 1.8rem; } .login-wrapper .login .login-group { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - padding: 2.4rem 0 0 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding: 2.4rem 0 0 0; } .login-wrapper .login .login-group .auth-source, .login-wrapper .login .login-group .checkbox, @@ -7387,489 +7357,488 @@ a.label > .badge { .login-wrapper .login .login-group .clr-form-control, .login-wrapper .login .login-group .password, .login-wrapper .login .login-group .username { - margin: 0.3rem 0 0.9rem 0; + margin: 0.3rem 0 0.9rem 0; } .login-wrapper .login .login-group .clr-control-container { - display: block; - width: 100%; + display: block; + width: 100%; } .login-wrapper .login .login-group .clr-control-container .clr-select, .login-wrapper .login .login-group .clr-control-container .clr-select-wrapper { - width: 100%; + width: 100%; } .login-wrapper - .login - .login-group - .clr-control-container - .clr-input-wrapper - > .clr-input { - width: 100%; + .login + .login-group + .clr-control-container + .clr-input-wrapper + > .clr-input { + width: 100%; } .login-wrapper .login .login-group .clr-control-container .clr-input-wrapper { - width: 100%; + width: 100%; } .login-wrapper - .login - .login-group - .clr-control-container - .clr-input-wrapper - > .clr-input-group { - max-width: 100%; - width: 100%; - padding-right: 0.48rem; + .login + .login-group + .clr-control-container + .clr-input-wrapper + > .clr-input-group { + max-width: 100%; + width: 100%; + padding-right: 0.48rem; } .login-wrapper - .login - .login-group - .clr-control-container - .clr-input-wrapper - > .clr-input-group - > .clr-input { - width: calc(100% - 1.2rem); + .login + .login-group + .clr-control-container + .clr-input-wrapper + > .clr-input-group + > .clr-input { + width: calc(100% - 1.2rem); } .login-wrapper .login .login-group .tooltip-validation { - margin-top: 0.3rem; + margin-top: 0.3rem; } .login-wrapper .login .login-group .tooltip-validation .password, .login-wrapper .login .login-group .tooltip-validation .username { - width: 100%; - margin-top: 0; + width: 100%; + margin-top: 0; } .login-wrapper .login .login-group .error { - display: none; - margin: 0.3rem 0 0 0; - padding: 0.45rem 0.6rem; - background: #c21d00; - background: var(--clr-login-error-background-color, #c21d00); - color: #fafafa; - color: var(--clr-login-background-color, #fafafa); - border-radius: 0.15rem; - border-radius: var(--clr-login-error-border-radius, 0.15rem); - line-height: 0.9rem; + display: none; + margin: 0.3rem 0 0 0; + padding: 0.45rem 0.6rem; + background: #c21d00; + background: var(--clr-login-error-background-color, #c21d00); + color: #fafafa; + color: var(--clr-login-background-color, #fafafa); + border-radius: 0.15rem; + border-radius: var(--clr-login-error-border-radius, 0.15rem); + line-height: 0.9rem; } .login-wrapper .login .login-group .error:before { - display: inline-block; - content: ''; - background: url('data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%3E.clr-i-outline%7Bfill%3A%23fafafa%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-circle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C6A12%2C12%2C0%2C1%2C0%2C30%2C18%2C12%2C12%2C0%2C0%2C0%2C18%2C6Zm0%2C22A10%2C10%2C0%2C1%2C1%2C28%2C18%2C10%2C10%2C0%2C0%2C1%2C18%2C28Z%22%3E%3C%2Fpath%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20d%3D%22M18%2C20.07a1.3%2C1.3%2C0%2C0%2C1-1.3-1.3v-6a1.3%2C1.3%2C0%2C1%2C1%2C2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C1%2C18%2C20.07Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20cx%3D%2217.95%22%20cy%3D%2223.02%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E'); - margin: 0.05rem 0.3rem 0 0; - height: 0.8rem; - width: 0.8rem; + display: inline-block; + content: ""; + background: url("data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%225%205%2026%2026%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%3Cdefs%3E%3Cstyle%3E.clr-i-outline%7Bfill%3A%23fafafa%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctitle%3Eexclamation-circle-line%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M18%2C6A12%2C12%2C0%2C1%2C0%2C30%2C18%2C12%2C12%2C0%2C0%2C0%2C18%2C6Zm0%2C22A10%2C10%2C0%2C1%2C1%2C28%2C18%2C10%2C10%2C0%2C0%2C1%2C18%2C28Z%22%3E%3C%2Fpath%3E%3Cpath%20class%3D%22clr-i-outline%20clr-i-outline-path-2%22%20d%3D%22M18%2C20.07a1.3%2C1.3%2C0%2C0%2C1-1.3-1.3v-6a1.3%2C1.3%2C0%2C1%2C1%2C2.6%2C0v6A1.3%2C1.3%2C0%2C0%2C1%2C18%2C20.07Z%22%3E%3C%2Fpath%3E%3Ccircle%20class%3D%22clr-i-outline%20clr-i-outline-path-3%22%20cx%3D%2217.95%22%20cy%3D%2223.02%22%20r%3D%221.5%22%3E%3C%2Fcircle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fsvg%3E"); + margin: 0.05rem 0.3rem 0 0; + height: 0.8rem; + width: 0.8rem; } .login-wrapper .login .login-group .error.active { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .login-wrapper .login .login-group .error.active:before { - -webkit-box-flex: 0; - -ms-flex: 0 0 0.8rem; - flex: 0 0 0.8rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 0.8rem; + flex: 0 0 0.8rem; } .login-wrapper .login .login-group .btn { - margin: 3.6rem 0 0 0; - max-width: none; + margin: 3.6rem 0 0 0; + max-width: none; } .login-wrapper .login .login-group .error + .btn { - margin: 1.2rem 0 0 0; + margin: 1.2rem 0 0 0; } .login-wrapper .login .login-group .signup { - margin-top: 0.6rem; - font-size: 0.7rem; - text-align: center; + margin-top: 0.6rem; + font-size: 0.7rem; + text-align: center; } .login-wrapper .login:after { - position: absolute; - content: ''; - display: block; - width: 0.05rem; - height: 100%; - background: #000; - background: var(--clr-login-panel-line-color, #000); - opacity: 0.1; - opacity: var(--clr-login-panel-line-opacity, 0.1); - top: 0; - right: -0.1rem; + position: absolute; + content: ""; + display: block; + width: 0.05rem; + height: 100%; + background: #000; + background: var(--clr-login-panel-line-color, #000); + opacity: 0.1; + opacity: var(--clr-login-panel-line-opacity, 0.1); + top: 0; + right: -0.1rem; } @media screen and (max-width: 768px) { - .login-wrapper { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - background: #fafafa; - background: var(--clr-login-background-color, #fafafa); - } - .login-wrapper .login { - width: 100%; - margin-left: 0; - padding: 1.2rem 20%; - } - .login-wrapper .login:after { - content: none; - } + .login-wrapper { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + background: #fafafa; + background: var(--clr-login-background-color, #fafafa); + } + .login-wrapper .login { + width: 100%; + margin-left: 0; + padding: 1.2rem 20%; + } + .login-wrapper .login:after { + content: none; + } } @media screen and (max-width: 576px) { - .login-wrapper .login { - padding: 1.2rem 15%; - } + .login-wrapper .login { + padding: 1.2rem 15%; + } } .main-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - height: 100vh; - background: #fafafa; - background: var(--clr-global-app-background, #fafafa); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + height: 100vh; + background: #fafafa; + background: var(--clr-global-app-background, #fafafa); } .main-container .alert.alert-app-level { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - overflow-x: hidden; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + overflow-x: hidden; } .main-container .header, .main-container header { - -webkit-box-flex: 0; - -ms-flex: 0 0 3rem; - flex: 0 0 3rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 3rem; + flex: 0 0 3rem; } .main-container .sub-nav, .main-container .subnav { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.8rem; - flex: 0 0 1.8rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.8rem; + flex: 0 0 1.8rem; } .main-container .u-main-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - overflow: hidden; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + overflow: hidden; } .main-container .content-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-height: 0.05rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-height: 0.05rem; } .main-container .content-container .content-area { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - padding: 1.2rem 1.2rem 1.2rem 1.2rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding: 1.2rem 1.2rem 1.2rem 1.2rem; } .main-container .content-container .content-area > :first-child { - margin-top: 0; + margin-top: 0; } .main-container .content-container .sidenav { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - overflow: hidden; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + overflow: hidden; } .main-container .content-container .clr-vertical-nav { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; } @media print { - .main-container { - height: auto; - } + .main-container { + height: auto; + } } body.no-scrolling, -body[cds-layout='no-scrolling'] { - overflow: hidden; +body[cds-layout="no-scrolling"] { + overflow: hidden; } body.no-scrolling .main-container .content-container .content-area, -body[cds-layout='no-scrolling'] - .main-container - .content-container - .content-area { - overflow: hidden; +body[cds-layout="no-scrolling"] + .main-container + .content-container + .content-area { + overflow: hidden; } :root { - --clr-modal-close-color: var(--clr-color-neutral-600); - --clr-modal-bg-color: var(--clr-color-neutral-0); - --clr-modal-content-box-shadow-color: rgba(0, 0, 0, 0.2); - --clr-modal-backdrop-color: var(--clr-color-neutral-900); - --clr-modal-backdrop-opacity: 0.85; - --clr-modal-border-radius: var(--clr-global-borderradius); - --clr-modal-title-color: var(--clr-h3-color); - --clr-modal-title-font-family: var(--clr-h3-font-family); - --clr-modal-title-font-weight: var(--clr-h3-font-weight); + --clr-modal-close-color: var(--clr-color-neutral-600); + --clr-modal-bg-color: var(--clr-color-neutral-0); + --clr-modal-content-box-shadow-color: rgba(0, 0, 0, 0.2); + --clr-modal-backdrop-color: var(--clr-color-neutral-900); + --clr-modal-backdrop-opacity: 0.85; + --clr-modal-border-radius: var(--clr-global-borderradius); + --clr-modal-title-color: var(--clr-h3-color); + --clr-modal-title-font-family: var(--clr-h3-font-family); + --clr-modal-title-font-weight: var(--clr-h3-font-weight); } .modal { - position: fixed; - top: 0; - bottom: 0; - right: 0; - left: 0; - z-index: 1050; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - padding: 2.4rem; + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + z-index: 1050; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 2.4rem; } @media screen and (max-width: 576px) { - .modal { - padding: 0.6rem; - } + .modal { + padding: 0.6rem; + } } .modal-dialog { - position: relative; - z-index: 1050; - width: 28.8rem; - max-width: 100%; + position: relative; + z-index: 1050; + width: 28.8rem; + max-width: 100%; } .modal-dialog.modal-sm { - width: 14.4rem; + width: 14.4rem; } .modal-dialog.modal-lg { - width: 43.2rem; + width: 43.2rem; } .modal-dialog.modal-xl { - width: 57.6rem; + width: 57.6rem; } .modal-dialog .modal-content { - padding: 1.2rem 1.2rem 1.2rem 1.2rem; - background-color: #fff; - background-color: var(--clr-modal-bg-color, #fff); - border-radius: 0.15rem; - border-radius: var(--clr-modal-border-radius, 0.15rem); - -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); - box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem - var(--clr-modal-content-box-shadow-color); - box-shadow: 0 0.05rem 0.1rem 0.1rem - var(--clr-modal-content-box-shadow-color); + padding: 1.2rem 1.2rem 1.2rem 1.2rem; + background-color: #fff; + background-color: var(--clr-modal-bg-color, #fff); + border-radius: 0.15rem; + border-radius: var(--clr-modal-border-radius, 0.15rem); + -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); + box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem + var(--clr-modal-content-box-shadow-color); + box-shadow: 0 0.05rem 0.1rem 0.1rem var(--clr-modal-content-box-shadow-color); } .modal-header, .modal-header--accessible { - border-bottom: none; - padding: 0 0 1.2rem 0; + border-bottom: none; + padding: 0 0 1.2rem 0; } .modal-header--accessible { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; } .modal-header .modal-title, .modal-header--accessible .modal-title { - color: #000; - color: var(--clr-modal-title-color, #000); - font-size: 1.1rem; - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-modal-title-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-weight: 200; - font-weight: var(--clr-modal-title-font-weight, 200); - line-height: 1.2rem; - letter-spacing: normal; - margin: 0; - padding: 0 0.15rem; + color: #000; + color: var(--clr-modal-title-color, #000); + font-size: 1.1rem; + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-modal-title-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-weight: 200; + font-weight: var(--clr-modal-title-font-weight, 200); + line-height: 1.2rem; + letter-spacing: normal; + margin: 0; + padding: 0 0.15rem; } .modal-header .close, .modal-header--accessible .close { - margin-top: -0.05rem; - margin-right: -0.25rem; - font-size: 1.3rem; - line-height: 1.2rem; + margin-top: -0.05rem; + margin-right: -0.25rem; + font-size: 1.3rem; + line-height: 1.2rem; } .modal-header .close cds-icon, .modal-header--accessible .close cds-icon { - fill: #8c8c8c; - fill: var(--clr-modal-close-color, #8c8c8c); - height: 1.2rem; - width: 1.2rem; + fill: #8c8c8c; + fill: var(--clr-modal-close-color, #8c8c8c); + height: 1.2rem; + width: 1.2rem; } .modal-body { - max-height: 70vh; - overflow-y: auto; - overflow-x: hidden; - padding: 0 0.15rem; + max-height: 70vh; + overflow-y: auto; + overflow-x: hidden; + padding: 0 0.15rem; } .modal-body > :first-child { - margin-top: 0; + margin-top: 0; } .modal-body > :last-child { - margin-bottom: 0; + margin-bottom: 0; } .modal-footer { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - padding: 1.2rem 0 0 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + padding: 1.2rem 0 0 0; } .modal-footer .btn { - margin: 0 0 0 0.6rem; + margin: 0 0 0 0.6rem; } @media screen and (max-width: 768px) and (orientation: landscape) { - .modal-body { - max-height: 55vh; - } + .modal-body { + max-height: 55vh; + } } @media screen and (max-width: 576px) { - .modal-content { - padding: 0.6rem 0 0.6rem 1.2rem; - } - .modal-header, - .modal-header--accessible { - padding: 0 1.2rem 0.6rem 0; - } - .modal-body { - max-height: 55vh; - } - .modal-footer { - padding: 0.6rem 1.2rem 0 0; - } + .modal-content { + padding: 0.6rem 0 0.6rem 1.2rem; + } + .modal-header, + .modal-header--accessible { + padding: 0 1.2rem 0.6rem 0; + } + .modal-body { + max-height: 55vh; + } + .modal-footer { + padding: 0.6rem 1.2rem 0 0; + } } .modal-backdrop { - position: fixed; - top: 0; - bottom: 0; - right: 0; - left: 0; - background-color: #333; - background-color: var(--clr-modal-backdrop-color, #333); - opacity: 0.85; - opacity: var(--clr-modal-backdrop-opacity, 0.85); - z-index: 1040; + position: fixed; + top: 0; + bottom: 0; + right: 0; + left: 0; + background-color: #333; + background-color: var(--clr-modal-backdrop-color, #333); + opacity: 0.85; + opacity: var(--clr-modal-backdrop-opacity, 0.85); + z-index: 1040; } .modal .modal-nav { - display: none; + display: none; } :root { - --clr-header-bg-color: var(--clr-color-neutral-900); - --clr-header-divider-opacity: 0.15; - --clr-header-nav-opacity: 0.65; - --clr-header-nav-hover-opacity: 1; - --clr-header-2-bg-color: #485a6a; - --clr-header-3-bg-color: var(--clr-color-secondary-action-1000); - --clr-header-4-bg-color: #247bae; - --clr-header-5-bg-color: var(--clr-color-action-800); - --clr-header-6-bg-color: var(--clr-color-action-1000); - --clr-header-7-bg-color: #304250; - --clr-header-font-color: var(--clr-color-neutral-50); - --clr-header-title-color: var(--clr-header-font-color); - --clr-header-title-font-weight: var(--clr-h5-font-weight); - --clr-header-title-font-family: var(--clr-h5-font-family); + --clr-header-bg-color: var(--clr-color-neutral-900); + --clr-header-divider-opacity: 0.15; + --clr-header-nav-opacity: 0.65; + --clr-header-nav-hover-opacity: 1; + --clr-header-2-bg-color: #485a6a; + --clr-header-3-bg-color: var(--clr-color-secondary-action-1000); + --clr-header-4-bg-color: #247bae; + --clr-header-5-bg-color: var(--clr-color-action-800); + --clr-header-6-bg-color: var(--clr-color-action-1000); + --clr-header-7-bg-color: #304250; + --clr-header-font-color: var(--clr-color-neutral-50); + --clr-header-title-color: var(--clr-header-font-color); + --clr-header-title-font-weight: var(--clr-h5-font-weight); + --clr-header-title-font-family: var(--clr-h5-font-family); } .header, header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); - background-color: #333; - background-color: var(--clr-header-bg-color, #333); - height: 3rem; - white-space: nowrap; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); + background-color: #333; + background-color: var(--clr-header-bg-color, #333); + height: 3rem; + white-space: nowrap; } .header.header-1, header.header-1 { - background-color: #333; - background-color: var(--clr-header-bg-color, #333); + background-color: #333; + background-color: var(--clr-header-bg-color, #333); } .header.header-2, header.header-2 { - background-color: #485a6a; - background-color: var(--clr-header-2-bg-color, #485a6a); + background-color: #485a6a; + background-color: var(--clr-header-2-bg-color, #485a6a); } .header.header-3, header.header-3 { - background-color: #320047; - background-color: var(--clr-header-3-bg-color, #320047); + background-color: #320047; + background-color: var(--clr-header-3-bg-color, #320047); } .header.header-4, header.header-4 { - background-color: #247bae; - background-color: var(--clr-header-4-bg-color, #247bae); + background-color: #247bae; + background-color: var(--clr-header-4-bg-color, #247bae); } .header.header-5, header.header-5 { - background-color: #00567a; - background-color: var(--clr-header-5-bg-color, #00567a); + background-color: #00567a; + background-color: var(--clr-header-5-bg-color, #00567a); } .header.header-6, header.header-6 { - background-color: #00364d; - background-color: var(--clr-header-6-bg-color, #00364d); + background-color: #00364d; + background-color: var(--clr-header-6-bg-color, #00364d); } .header.header-7, header.header-7 { - background-color: #304250; - background-color: var(--clr-header-7-bg-color, #304250); + background-color: #304250; + background-color: var(--clr-header-7-bg-color, #304250); } .header .branding, header .branding { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - min-width: 10.2rem; - padding: 0 1.2rem; - height: 3rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + min-width: 10.2rem; + padding: 0 1.2rem; + height: 3rem; } .header .branding > .nav-link, .header .branding > a, header .branding > .nav-link, header .branding > a { - display: -webkit-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - height: 3rem; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 3rem; } .header .branding > .nav-link:active, .header .branding > .nav-link:hover, @@ -7879,46 +7848,46 @@ header .branding > .nav-link:active, header .branding > .nav-link:hover, header .branding > a:active, header .branding > a:hover { - text-decoration: none; + text-decoration: none; } .header .branding > .nav-link:focus, .header .branding > a:focus, header .branding > .nav-link:focus, header .branding > a:focus { - outline-offset: -0.25rem; + outline-offset: -0.25rem; } .header .branding .clr-icon, .header .branding cds-icon, header .branding .clr-icon, header .branding cds-icon { - -webkit-box-flex: 0; - -ms-flex-positive: 0; - flex-grow: 0; - -ms-flex-negative: 0; - flex-shrink: 0; - height: 1.8rem; - width: 1.8rem; - margin-right: 0.45rem; + -webkit-box-flex: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + height: 1.8rem; + width: 1.8rem; + margin-right: 0.45rem; } .header .branding .title, header .branding .title { - color: #fafafa; - color: var(--clr-header-title-color, #fafafa); - font-weight: 400; - font-weight: var(--clr-header-title-font-weight, 400); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-header-title-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 0.8rem; - letter-spacing: 0.01em; - line-height: 3rem; - text-decoration: none; + color: #fafafa; + color: var(--clr-header-title-color, #fafafa); + font-weight: 400; + font-weight: var(--clr-header-title-font-weight, 400); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-header-title-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 0.8rem; + letter-spacing: 0.01em; + line-height: 3rem; + text-decoration: none; } .header .header-actions, .header .header-nav, @@ -7926,7 +7895,7 @@ header .branding .title { header .header-actions, header .header-nav, header .settings { - height: 3rem; + height: 3rem; } .header .header-actions .nav-text, .header .header-nav .nav-text, @@ -7934,7 +7903,7 @@ header .settings { header .header-actions .nav-text, header .header-nav .nav-text, header .settings .nav-text { - font-weight: 500; + font-weight: 500; } .header .header-actions cds-icon, .header .header-nav cds-icon, @@ -7942,8 +7911,8 @@ header .settings .nav-text { header .header-actions cds-icon, header .header-nav cds-icon, header .settings cds-icon { - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); } .header .header-actions .nav-icon, .header .header-nav .nav-icon, @@ -7951,8 +7920,8 @@ header .settings cds-icon { header .header-actions .nav-icon, header .header-nav .nav-icon, header .settings .nav-icon { - height: 3rem; - width: 3rem; + height: 3rem; + width: 3rem; } .header .header-actions .nav-link, .header .header-nav .nav-link, @@ -7960,14 +7929,14 @@ header .settings .nav-icon { header .header-actions .nav-link, header .header-nav .nav-link, header .settings .nav-link { - position: relative; - display: inline-block; - text-align: center; - padding: 0.9rem 1.2rem; - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); - opacity: 0.65; - opacity: var(--clr-header-nav-opacity, 0.65); + position: relative; + display: inline-block; + text-align: center; + padding: 0.9rem 1.2rem; + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); + opacity: 0.65; + opacity: var(--clr-header-nav-opacity, 0.65); } .header .header-actions .nav-link:active, .header .header-actions .nav-link:hover, @@ -7981,7 +7950,7 @@ header .header-nav .nav-link:active, header .header-nav .nav-link:hover, header .settings .nav-link:active, header .settings .nav-link:hover { - text-decoration: none; + text-decoration: none; } .header .header-actions .nav-link:enabled:hover, .header .header-nav .nav-link:enabled:hover, @@ -7989,8 +7958,8 @@ header .settings .nav-link:hover { header .header-actions .nav-link:enabled:hover, header .header-nav .nav-link:enabled:hover, header .settings .nav-link:enabled:hover { - opacity: 1; - opacity: var(--clr-header-nav-hover-opacity, 1); + opacity: 1; + opacity: var(--clr-header-nav-hover-opacity, 1); } .header .header-actions .nav-link:disabled, .header .header-nav .nav-link:disabled, @@ -7998,7 +7967,7 @@ header .settings .nav-link:enabled:hover { header .header-actions .nav-link:disabled, header .header-nav .nav-link:disabled, header .settings .nav-link:disabled { - cursor: not-allowed; + cursor: not-allowed; } .header .header-actions .nav-link .fa, .header .header-actions .nav-link .nav-icon, @@ -8012,7 +7981,7 @@ header .header-nav .nav-link .fa, header .header-nav .nav-link .nav-icon, header .settings .nav-link .fa, header .settings .nav-link .nav-icon { - font-size: 1.1rem; + font-size: 1.1rem; } .header .header-actions .nav-link cds-icon, .header .header-nav .nav-link cds-icon, @@ -8020,13 +7989,13 @@ header .settings .nav-link .nav-icon { header .header-actions .nav-link cds-icon, header .header-nav .nav-link cds-icon, header .settings .nav-link cds-icon { - position: absolute; - top: 50%; - left: 50%; - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - height: 1.2rem; - width: 1.2rem; + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + height: 1.2rem; + width: 1.2rem; } .header .header-actions .nav-link.nav-icon-text cds-icon, .header .header-nav .nav-link.nav-icon-text cds-icon, @@ -8034,12 +8003,12 @@ header .settings .nav-link cds-icon { header .header-actions .nav-link.nav-icon-text cds-icon, header .header-nav .nav-link.nav-icon-text cds-icon, header .settings .nav-link.nav-icon-text cds-icon { - position: relative; - top: auto; - left: auto; - -webkit-transform: none; - transform: none; - margin-left: 1.2rem; + position: relative; + top: auto; + left: auto; + -webkit-transform: none; + transform: none; + margin-left: 1.2rem; } .header .header-actions .nav-link.nav-icon-text .nav-text, .header .header-nav .nav-link.nav-icon-text .nav-text, @@ -8047,8 +8016,8 @@ header .settings .nav-link.nav-icon-text cds-icon { header .header-actions .nav-link.nav-icon-text .nav-text, header .header-nav .nav-link.nav-icon-text .nav-text, header .settings .nav-link.nav-icon-text .nav-text { - margin-left: 0; - padding-left: 0.3rem; + margin-left: 0; + padding-left: 0.3rem; } .header .header-actions .nav-link .nav-icon + .nav-text, .header .header-nav .nav-link .nav-icon + .nav-text, @@ -8056,7 +8025,7 @@ header .settings .nav-link.nav-icon-text .nav-text { header .header-actions .nav-link .nav-icon + .nav-text, header .header-nav .nav-link .nav-icon + .nav-text, header .settings .nav-link .nav-icon + .nav-text { - display: none; + display: none; } .header .header-actions .nav-link.active, .header .header-nav .nav-link.active, @@ -8064,8 +8033,8 @@ header .settings .nav-link .nav-icon + .nav-text { header .header-actions .nav-link.active, header .header-nav .nav-link.active, header .settings .nav-link.active { - background: rgba(255, 255, 255, 0.15); - opacity: 1; + background: rgba(255, 255, 255, 0.15); + opacity: 1; } .header .header-actions .nav-link:focus, .header .header-nav .nav-link:focus, @@ -8073,184 +8042,184 @@ header .settings .nav-link.active { header .header-actions .nav-link:focus, header .header-nav .nav-link:focus, header .settings .nav-link:focus { - outline-offset: -0.25rem; + outline-offset: -0.25rem; } .header .header-nav, header .header-nav { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; } .header .header-nav:last-child > .nav-link:last-child::after, header .header-nav:last-child > .nav-link:last-child::after { - content: none; + content: none; } .header .header-nav .nav-link:first-of-type, .header .header-nav .nav-link:last-of-type, header .header-nav .nav-link:first-of-type, header .header-nav .nav-link:last-of-type { - position: relative; + position: relative; } .header .header-nav .nav-link:first-of-type::before, .header .header-nav .nav-link:last-of-type::after, header .header-nav .nav-link:first-of-type::before, header .header-nav .nav-link:last-of-type::after { - display: inline-block; - position: absolute; - content: ''; - background: #fafafa; - background: var(--clr-header-font-color, #fafafa); - opacity: 0.15; - opacity: var(--clr-header-divider-opacity, 0.15); - opacity: 0.15; - height: 2rem; - width: 0.05rem; - width: var(--clr-global-borderwidth, 0.05rem); - top: 0.5rem; - left: 0; - left: auto; + display: inline-block; + position: absolute; + content: ""; + background: #fafafa; + background: var(--clr-header-font-color, #fafafa); + opacity: 0.15; + opacity: var(--clr-header-divider-opacity, 0.15); + opacity: 0.15; + height: 2rem; + width: 0.05rem; + width: var(--clr-global-borderwidth, 0.05rem); + top: 0.5rem; + left: 0; + left: auto; } .header .header-nav .nav-link:first-of-type::before, header .header-nav .nav-link:first-of-type::before { - left: 0; + left: 0; } .header .header-nav .nav-link:last-of-type::after, header .header-nav .nav-link:last-of-type::after { - right: 0; + right: 0; } .header .header-nav .nav-link.active:first-of-type::before, .header .header-nav .nav-link.active:last-of-type::after, header .header-nav .nav-link.active:first-of-type::before, header .header-nav .nav-link.active:last-of-type::after { - content: none; + content: none; } -.header .header-actions, -.header .settings, -header .header-actions, -header .settings { - -webkit-box-flex: 1; - -ms-flex: 1 0 auto; - flex: 1 0 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; +.header .header-actions, +.header .settings, +header .header-actions, +header .settings { + -webkit-box-flex: 1; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .header .header-actions > .dropdown > .dropdown-toggle, .header .settings > .dropdown > .dropdown-toggle, header .header-actions > .dropdown > .dropdown-toggle, header .settings > .dropdown > .dropdown-toggle { - position: relative; - line-height: 3rem; - height: 3rem; - outline-offset: -0.25rem; - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); - opacity: 0.65; - opacity: var(--clr-header-nav-opacity, 0.65); + position: relative; + line-height: 3rem; + height: 3rem; + outline-offset: -0.25rem; + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); + opacity: 0.65; + opacity: var(--clr-header-nav-opacity, 0.65); } .header .header-actions > .dropdown > .dropdown-toggle:enabled:hover, .header .settings > .dropdown > .dropdown-toggle:enabled:hover, header .header-actions > .dropdown > .dropdown-toggle:enabled:hover, header .settings > .dropdown > .dropdown-toggle:enabled:hover { - opacity: 1; - opacity: var(--clr-header-nav-hover-opacity, 1); + opacity: 1; + opacity: var(--clr-header-nav-hover-opacity, 1); } .header .header-actions > .dropdown > .dropdown-toggle:disabled, .header .settings > .dropdown > .dropdown-toggle:disabled, header .header-actions > .dropdown > .dropdown-toggle:disabled, header .settings > .dropdown > .dropdown-toggle:disabled { - cursor: not-allowed; + cursor: not-allowed; } .header - .header-actions - > .dropdown - .dropdown-toggle.nav-icon - cds-icon:not([shape^='angle']), + .header-actions + > .dropdown + .dropdown-toggle.nav-icon + cds-icon:not([shape^="angle"]), .header - .settings - > .dropdown - .dropdown-toggle.nav-icon - cds-icon:not([shape^='angle']), + .settings + > .dropdown + .dropdown-toggle.nav-icon + cds-icon:not([shape^="angle"]), header - .header-actions - > .dropdown - .dropdown-toggle.nav-icon - cds-icon:not([shape^='angle']), + .header-actions + > .dropdown + .dropdown-toggle.nav-icon + cds-icon:not([shape^="angle"]), header - .settings - > .dropdown - .dropdown-toggle.nav-icon - cds-icon:not([shape^='angle']) { - position: absolute; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); - height: 1.1rem; - width: 1.1rem; - right: 1.2rem; + .settings + > .dropdown + .dropdown-toggle.nav-icon + cds-icon:not([shape^="angle"]) { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); + height: 1.1rem; + width: 1.1rem; + right: 1.2rem; } .header - .header-actions - > .dropdown - .dropdown-toggle.nav-icon - cds-icon[shape^='angle'], + .header-actions + > .dropdown + .dropdown-toggle.nav-icon + cds-icon[shape^="angle"], .header - .settings - > .dropdown - .dropdown-toggle.nav-icon - cds-icon[shape^='angle'], + .settings + > .dropdown + .dropdown-toggle.nav-icon + cds-icon[shape^="angle"], header - .header-actions - > .dropdown - .dropdown-toggle.nav-icon - cds-icon[shape^='angle'], + .header-actions + > .dropdown + .dropdown-toggle.nav-icon + cds-icon[shape^="angle"], header - .settings - > .dropdown - .dropdown-toggle.nav-icon - cds-icon[shape^='angle'] { - right: 0.6rem; + .settings + > .dropdown + .dropdown-toggle.nav-icon + cds-icon[shape^="angle"] { + right: 0.6rem; } .header .header-actions > .dropdown .dropdown-toggle.nav-text, .header .settings > .dropdown .dropdown-toggle.nav-text, header .header-actions > .dropdown .dropdown-toggle.nav-text, header .settings > .dropdown .dropdown-toggle.nav-text { - padding: 0 1.8rem 0 1.2rem; + padding: 0 1.8rem 0 1.2rem; } .header - .header-actions - > .dropdown - .dropdown-toggle.nav-text - cds-icon[shape^='angle'], + .header-actions + > .dropdown + .dropdown-toggle.nav-text + cds-icon[shape^="angle"], .header - .settings - > .dropdown - .dropdown-toggle.nav-text - cds-icon[shape^='angle'], + .settings + > .dropdown + .dropdown-toggle.nav-text + cds-icon[shape^="angle"], header - .header-actions - > .dropdown - .dropdown-toggle.nav-text - cds-icon[shape^='angle'], + .header-actions + > .dropdown + .dropdown-toggle.nav-text + cds-icon[shape^="angle"], header - .settings - > .dropdown - .dropdown-toggle.nav-text - cds-icon[shape^='angle'] { - right: 1.2rem; + .settings + > .dropdown + .dropdown-toggle.nav-text + cds-icon[shape^="angle"] { + right: 1.2rem; } .header .header-actions > .dropdown .dropdown-toggle.nav-icon, .header .settings > .dropdown .dropdown-toggle.nav-icon, header .header-actions > .dropdown .dropdown-toggle.nav-icon, header .settings > .dropdown .dropdown-toggle.nav-icon { - width: 3rem; - padding-right: 0; + width: 3rem; + padding-right: 0; } .header .header-actions > .dropdown.bottom-left > .dropdown-menu, .header .header-actions > .dropdown.bottom-right > .dropdown-menu, @@ -8260,134 +8229,207 @@ header .header-actions > .dropdown.bottom-left > .dropdown-menu, header .header-actions > .dropdown.bottom-right > .dropdown-menu, header .settings > .dropdown.bottom-left > .dropdown-menu, header .settings > .dropdown.bottom-right > .dropdown-menu { - top: 85%; + top: 85%; } .header .header-actions > .dropdown:last-child.bottom-right > .dropdown-menu, .header .settings > .dropdown:last-child.bottom-right > .dropdown-menu, header .header-actions > .dropdown:last-child.bottom-right > .dropdown-menu, header .settings > .dropdown:last-child.bottom-right > .dropdown-menu { - right: 0.15rem; + right: 0.15rem; } .header .header-actions > .dropdown .dropdown-menu, .header .settings > .dropdown .dropdown-menu, header .header-actions > .dropdown .dropdown-menu, header .settings > .dropdown .dropdown-menu { - margin-top: -0.2rem; - left: auto; - right: 0; + margin-top: -0.2rem; + left: auto; + right: 0; } .header .header-actions > .dropdown :last-child.dropdown-menu, .header .settings > .dropdown :last-child.dropdown-menu, header .header-actions > .dropdown :last-child.dropdown-menu, header .settings > .dropdown :last-child.dropdown-menu { - margin-right: 0.4rem; + margin-right: 0.4rem; } .header .search, .header .search-box, header .search, header .search-box { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - max-width: 14.4rem; - padding: 0; - height: 3rem; - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); - opacity: 0.65; - opacity: var(--clr-header-nav-opacity, 0.65); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + max-width: 14.4rem; + padding: 0; + height: 3rem; + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); + opacity: 0.65; + opacity: var(--clr-header-nav-opacity, 0.65); } .header .search-box:enabled:hover, .header .search:enabled:hover, header .search-box:enabled:hover, header .search:enabled:hover { - opacity: 1; - opacity: var(--clr-header-nav-hover-opacity, 1); + opacity: 1; + opacity: var(--clr-header-nav-hover-opacity, 1); } .header .search-box:disabled, .header .search:disabled, header .search-box:disabled, header .search:disabled { - cursor: not-allowed; + cursor: not-allowed; } .header .search-box > .nav-icon, .header .search > .nav-icon, header .search-box > .nav-icon, header .search > .nav-icon { - margin: 0 0.3rem 0.15rem 1.2rem; + margin: 0 0.3rem 0.15rem 1.2rem; } .header .search label, .header .search-box label, header .search label, header .search-box label { - display: inline-block; - height: 3rem; - line-height: 3rem; - padding-left: 1.2rem; - text-align: center; + display: inline-block; + height: 3rem; + line-height: 3rem; + padding-left: 1.2rem; + text-align: center; } .header .search label::before, .header .search-box label::before, header .search label::before, header .search-box label::before { - display: inline-block; - content: ''; - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2036%2036%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23ffffff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3ESearch%3C%2Ftitle%3E%3Cg%20id%3D%22icons%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M15%2C4.05A10.95%2C10.95%2C0%2C1%2C1%2C4.05%2C15%2C11%2C11%2C0%2C0%2C1%2C15%2C4.05M15%2C2A13%2C13%2C0%2C1%2C0%2C28%2C15%2C13%2C13%2C0%2C0%2C0%2C15%2C2Z%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20%20d%3D%22M33.71%2C32.29l-7.37-7.42-1.42%2C1.41%2C7.37%2C7.42a1%2C1%2C0%2C1%2C0%2C1.42-1.41Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E'); - background-repeat: no-repeat; - background-size: contain; - cursor: pointer; - height: 1rem; - width: 1rem; - margin: 1rem 0 0; - vertical-align: top; + display: inline-block; + content: ""; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2036%2036%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23ffffff%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3ESearch%3C%2Ftitle%3E%3Cg%20id%3D%22icons%22%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M15%2C4.05A10.95%2C10.95%2C0%2C1%2C1%2C4.05%2C15%2C11%2C11%2C0%2C0%2C1%2C15%2C4.05M15%2C2A13%2C13%2C0%2C1%2C0%2C28%2C15%2C13%2C13%2C0%2C0%2C0%2C15%2C2Z%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20%20d%3D%22M33.71%2C32.29l-7.37-7.42-1.42%2C1.41%2C7.37%2C7.42a1%2C1%2C0%2C1%2C0%2C1.42-1.41Z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + background-repeat: no-repeat; + background-size: contain; + cursor: pointer; + height: 1rem; + width: 1rem; + margin: 1rem 0 0; + vertical-align: top; } .header .search label input, .header .search-box label input, header .search label input, header .search-box label input { - line-height: 1.2rem; - margin: 0.9rem 0; -} -.header .search input[type='text'], -.header .search-box input[type='text'], -header .search input[type='text'], -header .search-box input[type='text'] { - border: none; - background: 0 0; - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); - padding: 0; - vertical-align: middle; -} -.header .search input[type='text']:active, -.header .search input[type='text']:focus, -.header .search-box input[type='text']:active, -.header .search-box input[type='text']:focus, -header .search input[type='text']:active, -header .search input[type='text']:focus, -header .search-box input[type='text']:active, -header .search-box input[type='text']:focus { - background: 0 0; + line-height: 1.2rem; + margin: 0.9rem 0; +} +.header .search input[type="text"], +.header .search-box input[type="text"], +header .search input[type="text"], +header .search-box input[type="text"] { + border: none; + background: 0 0; + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); + padding: 0; + vertical-align: middle; +} +.header .search input[type="text"]:active, +.header .search input[type="text"]:focus, +.header .search-box input[type="text"]:active, +.header .search-box input[type="text"]:focus, +header .search input[type="text"]:active, +header .search input[type="text"]:focus, +header .search-box input[type="text"]:active, +header .search-box input[type="text"]:focus { + background: 0 0; } .header .branding + .search, .header .branding + .search-box, header .branding + .search, header .branding + .search-box { - position: relative; + position: relative; } .header .branding + .search-box::after, .header .branding + .search::after, header .branding + .search-box::after, header .branding + .search::after { + display: inline-block; + position: absolute; + content: ""; + background: #fafafa; + background: var(--clr-header-font-color, #fafafa); + opacity: 0.15; + opacity: var(--clr-header-divider-opacity, 0.15); + opacity: 0.15; + height: 2rem; + width: 0.05rem; + width: var(--clr-global-borderwidth, 0.05rem); + top: 0.5rem; + left: 0; +} +@media screen and (max-width: 768px) { + .header .search, + .header .search-box, + header .search, + header .search-box { + -webkit-box-flex: 1; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + max-width: none; + } + .header .search label, + .header .search-box label, + header .search label, + header .search-box label { + padding: 0; + width: 3rem; + } + .header .search label::before, + .header .search-box label::before, + header .search label::before, + header .search-box label::before { + left: 1rem; + } + .header .search label input, + .header .search-box label input, + header .search label input, + header .search-box label input { + display: none; + } + .header .branding + .search-box::after, + .header .branding + .search::after, + header .branding + .search-box::after, + header .branding + .search::after { + content: none; + } + .header .search + .header-actions, + .header .search + .settings, + .header .search-box + .header-actions, + .header .search-box + .settings, + header .search + .header-actions, + header .search + .settings, + header .search-box + .header-actions, + header .search-box + .settings { + position: relative; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + } + .header .search + .header-actions::after, + .header .search + .settings::after, + .header .search-box + .header-actions::after, + .header .search-box + .settings::after, + header .search + .header-actions::after, + header .search + .settings::after, + header .search-box + .header-actions::after, + header .search-box + .settings::after { display: inline-block; position: absolute; - content: ''; + content: ""; background: #fafafa; background: var(--clr-header-font-color, #fafafa); opacity: 0.15; @@ -8398,908 +8440,1408 @@ header .branding + .search::after { width: var(--clr-global-borderwidth, 0.05rem); top: 0.5rem; left: 0; -} -@media screen and (max-width: 768px) { - .header .search, - .header .search-box, - header .search, - header .search-box { - -webkit-box-flex: 1; - -ms-flex: 1 0 auto; - flex: 1 0 auto; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - max-width: none; - } - .header .search label, - .header .search-box label, - header .search label, - header .search-box label { - padding: 0; - width: 3rem; - } - .header .search label::before, - .header .search-box label::before, - header .search label::before, - header .search-box label::before { - left: 1rem; - } - .header .search label input, - .header .search-box label input, - header .search label input, - header .search-box label input { - display: none; - } - .header .branding + .search-box::after, - .header .branding + .search::after, - header .branding + .search-box::after, - header .branding + .search::after { - content: none; - } - .header .search + .header-actions, - .header .search + .settings, - .header .search-box + .header-actions, - .header .search-box + .settings, - header .search + .header-actions, - header .search + .settings, - header .search-box + .header-actions, - header .search-box + .settings { - position: relative; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - } - .header .search + .header-actions::after, - .header .search + .settings::after, - .header .search-box + .header-actions::after, - .header .search-box + .settings::after, - header .search + .header-actions::after, - header .search + .settings::after, - header .search-box + .header-actions::after, - header .search-box + .settings::after { - display: inline-block; - position: absolute; - content: ''; - background: #fafafa; - background: var(--clr-header-font-color, #fafafa); - opacity: 0.15; - opacity: var(--clr-header-divider-opacity, 0.15); - opacity: 0.15; - height: 2rem; - width: 0.05rem; - width: var(--clr-global-borderwidth, 0.05rem); - top: 0.5rem; - left: 0; - } + } } a.link-normal:link { - color: #0072a3; - color: var(--clr-link-color, #0072a3); - text-decoration: none; + color: #0072a3; + color: var(--clr-link-color, #0072a3); + text-decoration: none; } a.link-hovered:link { - color: #0072a3; - color: var(--clr-link-hover-color, #0072a3); - text-decoration: underline; + color: #0072a3; + color: var(--clr-link-hover-color, #0072a3); + text-decoration: underline; } a.link-clicked:link { - color: #9e57bc; - color: var(--clr-link-active-color, #9e57bc); - text-decoration: underline; + color: #9e57bc; + color: var(--clr-link-active-color, #9e57bc); + text-decoration: underline; } a.link-visited:link { - color: #5659b8; - color: var(--clr-link-visited-color, #5659b8); - text-decoration: none; + color: #5659b8; + color: var(--clr-link-visited-color, #5659b8); + text-decoration: none; } .nav { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - height: 1.8rem; - list-style-type: none; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-shadow: 0 -0.05rem 0 #ccc inset; - box-shadow: 0 -0.05rem 0 #ccc inset; - margin: 0; - width: 100%; - white-space: nowrap; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + height: 1.8rem; + list-style-type: none; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-shadow: 0 -0.05rem 0 #ccc inset; + box-shadow: 0 -0.05rem 0 #ccc inset; + margin: 0; + width: 100%; + white-space: nowrap; } .nav .nav-item { - display: inline-block; - margin-right: 1.2rem; + display: inline-block; + margin-right: 1.2rem; } .nav .nav-item.active > .nav-link { - color: #000; - color: var(--clr-nav-link-active-color, #000); - -webkit-box-shadow: 0 -0.05rem 0 #ccc inset; - box-shadow: 0 -0.05rem 0 #ccc inset; - -webkit-box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; - box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; + color: #000; + color: var(--clr-nav-link-active-color, #000); + -webkit-box-shadow: 0 -0.05rem 0 #ccc inset; + box-shadow: 0 -0.05rem 0 #ccc inset; + -webkit-box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; + box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; } .nav .nav-link { - color: #8c8c8c; - color: var(--clr-nav-link-color, #8c8c8c); - font-size: 0.7rem; - font-weight: 400; - font-weight: var(--clr-nav-link-font-weight, 400); - letter-spacing: normal; - line-height: 1.8rem; - display: inline-block; - padding: 0 0.15rem; - -webkit-box-shadow: none; - box-shadow: none; + color: #8c8c8c; + color: var(--clr-nav-link-color, #8c8c8c); + font-size: 0.7rem; + font-weight: 400; + font-weight: var(--clr-nav-link-font-weight, 400); + letter-spacing: normal; + line-height: 1.8rem; + display: inline-block; + padding: 0 0.15rem; + -webkit-box-shadow: none; + box-shadow: none; } .nav .nav-link.btn { - text-transform: none; - margin: 0; - margin-bottom: -0.05rem; - border-radius: 0; + text-transform: none; + margin: 0; + margin-bottom: -0.05rem; + border-radius: 0; } .nav .nav-link:active, .nav .nav-link:focus, .nav .nav-link:hover { - color: inherit; + color: inherit; } .nav .nav-link.active, .nav .nav-link:hover { - -webkit-box-shadow: 0 -0.15rem 0 #0072a3 inset; - box-shadow: 0 -0.15rem 0 #0072a3 inset; - -webkit-box-shadow: 0 -0.15rem 0 var( - --clr-nav-active-box-shadow-color, - #0072a3 - ) inset; - box-shadow: 0 -0.15rem 0 var(--clr-nav-active-box-shadow-color, #0072a3) inset; - -webkit-transition: -webkit-box-shadow 0.2s ease-in; - transition: -webkit-box-shadow 0.2s ease-in; - transition: box-shadow 0.2s ease-in; - transition: - box-shadow 0.2s ease-in, - -webkit-box-shadow 0.2s ease-in; + -webkit-box-shadow: 0 -0.15rem 0 #0072a3 inset; + box-shadow: 0 -0.15rem 0 #0072a3 inset; + -webkit-box-shadow: 0 -0.15rem 0 + var(--clr-nav-active-box-shadow-color, #0072a3) inset; + box-shadow: 0 -0.15rem 0 var(--clr-nav-active-box-shadow-color, #0072a3) inset; + -webkit-transition: -webkit-box-shadow 0.2s ease-in; + transition: -webkit-box-shadow 0.2s ease-in; + transition: box-shadow 0.2s ease-in; + transition: + box-shadow 0.2s ease-in, + -webkit-box-shadow 0.2s ease-in; } .nav .nav-link.active, .nav .nav-link:active, .nav .nav-link:focus, .nav .nav-link:hover { - text-decoration: none; + text-decoration: none; } .nav .nav-link.active { - color: #000; - color: var(--clr-nav-link-active-color, #000); - font-weight: 400; - font-weight: var(--clr-nav-link-active-font-weight, 400); + color: #000; + color: var(--clr-nav-link-active-color, #000); + font-weight: 400; + font-weight: var(--clr-nav-link-active-font-weight, 400); } .nav .nav-link.nav-item { - margin-right: 1.2rem; + margin-right: 1.2rem; } :root { - --clr-subnav-bg-color: var(--clr-color-neutral-0); - --clr-nav-box-shadow-color: var(--clr-color-neutral-400); + --clr-subnav-bg-color: var(--clr-color-neutral-0); + --clr-nav-box-shadow-color: var(--clr-color-neutral-400); } .sub-nav, .subnav { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-shadow: 0 -0.05rem 0 #ccc inset; - box-shadow: 0 -0.05rem 0 #ccc inset; - -webkit-box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; - box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - background-color: #fff; - background-color: var(--clr-subnav-bg-color, #fff); - height: 1.8rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-shadow: 0 -0.05rem 0 #ccc inset; + box-shadow: 0 -0.05rem 0 #ccc inset; + -webkit-box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; + box-shadow: 0 -0.05rem 0 var(--clr-nav-box-shadow-color, #ccc) inset; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #fff; + background-color: var(--clr-subnav-bg-color, #fff); + height: 1.8rem; } .sub-nav .nav, .subnav .nav { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - padding-left: 1.2rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding-left: 1.2rem; } .sub-nav aside, .subnav aside { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - height: 1.8rem; - padding: 0 1.2rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 1.8rem; + padding: 0 1.2rem; } .sub-nav aside > :last-child, .subnav aside > :last-child { - margin-right: 0; - padding-right: 0; + margin-right: 0; + padding-right: 0; } :root { - --clr-sidenav-border-color: var(--clr-color-neutral-400); - --clr-sidenav-border-width: var(--clr-global-borderwidth); - --clr-sidenav-link-hover-color: var(--clr-color-neutral-200); - --clr-sidenav-link-active-color: var(--clr-color-neutral-1000); - --clr-sidenav-link-active-bg-color: var(--clr-global-selection-color); - --clr-sidenav-link-active-border-radius: var(--clr-global-borderradius); - --clr-sidenav-header-color: var(--clr-h6-color); - --clr-sidenav-header-font-weight: var(--clr-h6-font-weight); - --clr-sidenav-header-font-family: var(--clr-h6-font-family); - --clr-sidenav-color: var(--clr-p1-color); - --clr-sidenav-font-weight: var(--clr-p1-font-weight); + --clr-sidenav-border-color: var(--clr-color-neutral-400); + --clr-sidenav-border-width: var(--clr-global-borderwidth); + --clr-sidenav-link-hover-color: var(--clr-color-neutral-200); + --clr-sidenav-link-active-color: var(--clr-color-neutral-1000); + --clr-sidenav-link-active-bg-color: var(--clr-global-selection-color); + --clr-sidenav-link-active-border-radius: var(--clr-global-borderradius); + --clr-sidenav-header-color: var(--clr-h6-color); + --clr-sidenav-header-font-weight: var(--clr-h6-font-weight); + --clr-sidenav-header-font-family: var(--clr-h6-font-family); + --clr-sidenav-color: var(--clr-p1-color); + --clr-sidenav-font-weight: var(--clr-p1-font-weight); } .sidenav { - line-height: 1.2rem; - max-width: 15.6rem; - min-width: 10.8rem; - width: 18%; - border-right: 0.05rem solid #ccc; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; + line-height: 1.2rem; + max-width: 15.6rem; + min-width: 10.8rem; + width: 18%; + border-right: 0.05rem solid #ccc; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .sidenav .sidenav-content { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - overflow-x: hidden; - padding-bottom: 1.2rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + overflow-x: hidden; + padding-bottom: 1.2rem; } .sidenav .sidenav-content .nav-link { - border-radius: 0; - border-top-left-radius: 0.15rem; - border-top-left-radius: var( - --clr-sidenav-link-active-border-radius, - 0.15rem - ); - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var( - --clr-sidenav-link-active-border-radius, - 0.15rem - ); - display: inline-block; - color: inherit; - cursor: pointer; - text-decoration: none; - width: 100%; + border-radius: 0; + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-sidenav-link-active-border-radius, 0.15rem); + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var( + --clr-sidenav-link-active-border-radius, + 0.15rem + ); + display: inline-block; + color: inherit; + cursor: pointer; + text-decoration: none; + width: 100%; } .sidenav .sidenav-content > .nav-link { - margin: 1.2rem 0 0 1.5rem; - padding-left: 0.6rem; - color: #333; - color: var(--clr-sidenav-header-color, #333); - font-weight: 500; - font-weight: var(--clr-sidenav-header-font-weight, 500); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-sidenav-header-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 0.7rem; - line-height: 1.2rem; - letter-spacing: normal; + margin: 1.2rem 0 0 1.5rem; + padding-left: 0.6rem; + color: #333; + color: var(--clr-sidenav-header-color, #333); + font-weight: 500; + font-weight: var(--clr-sidenav-header-font-weight, 500); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-sidenav-header-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 0.7rem; + line-height: 1.2rem; + letter-spacing: normal; } .sidenav .sidenav-content > .nav-link:hover { - background: #e8e8e8; - background: var(--clr-sidenav-link-hover-color, #e8e8e8); + background: #e8e8e8; + background: var(--clr-sidenav-link-hover-color, #e8e8e8); } .sidenav .sidenav-content > .nav-link.active { - background: #d8e3e9; - background: var(--clr-sidenav-link-active-bg-color, #d8e3e9); - color: #000; - color: var(--clr-sidenav-link-active-color, #000); + background: #d8e3e9; + background: var(--clr-sidenav-link-active-bg-color, #d8e3e9); + color: #000; + color: var(--clr-sidenav-link-active-color, #000); } .sidenav .nav-group { - color: #666; - color: var(--clr-sidenav-color, #666); - font-weight: 400; - font-weight: var(--clr-sidenav-font-weight, 400); - font-size: 0.7rem; - letter-spacing: normal; - margin-top: 1.2rem; - width: 100%; + color: #666; + color: var(--clr-sidenav-color, #666); + font-weight: 400; + font-weight: var(--clr-sidenav-font-weight, 400); + font-size: 0.7rem; + letter-spacing: normal; + margin-top: 1.2rem; + width: 100%; } .sidenav .nav-group .nav-list, .sidenav .nav-group label { - padding: 0 0 0 1.8rem; - cursor: pointer; - display: inline-block; - width: 100%; - margin: 0 0.3rem; + padding: 0 0 0 1.8rem; + cursor: pointer; + display: inline-block; + width: 100%; + margin: 0 0.3rem; } .sidenav .nav-group .nav-list { - list-style: none; - margin-top: 0; + list-style: none; + margin-top: 0; } .sidenav .nav-group .nav-list .nav-link { - line-height: 0.8rem; - padding: 0.2rem 0 0.2rem 0.6rem; + line-height: 0.8rem; + padding: 0.2rem 0 0.2rem 0.6rem; } .sidenav .nav-group .nav-list .nav-link:hover { - background: #e8e8e8; - background: var(--clr-sidenav-link-hover-color, #e8e8e8); + background: #e8e8e8; + background: var(--clr-sidenav-link-hover-color, #e8e8e8); } .sidenav .nav-group .nav-list .nav-link.active { - background: #d8e3e9; - background: var(--clr-sidenav-link-active-bg-color, #d8e3e9); - color: #000; - color: var(--clr-sidenav-link-active-color, #000); + background: #d8e3e9; + background: var(--clr-sidenav-link-active-bg-color, #d8e3e9); + color: #000; + color: var(--clr-sidenav-link-active-color, #000); } .sidenav .nav-group label { - color: #333; - color: var(--clr-sidenav-header-color, #333); - font-weight: 500; - font-weight: var(--clr-sidenav-header-font-weight, 500); - font-family: Metropolis, 'Avenir Next', 'Helvetica Neue', Arial, sans-serif; - font-family: var( - --clr-sidenav-header-font-family, - Metropolis, - 'Avenir Next', - 'Helvetica Neue', - Arial, - sans-serif - ); - font-size: 0.7rem; - line-height: 1.2rem; - letter-spacing: normal; -} -.sidenav .nav-group input[type='checkbox'] { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); - clip-path: inset(50%); - padding: 0; - border: 0; - height: 1px; - width: 1px; - overflow: hidden; - white-space: nowrap; - top: 0; - left: 0; -} -.sidenav .nav-group input[type='checkbox']:focus + label { - outline: #3b99fc auto 0.25rem; + color: #333; + color: var(--clr-sidenav-header-color, #333); + font-weight: 500; + font-weight: var(--clr-sidenav-header-font-weight, 500); + font-family: Metropolis, "Avenir Next", "Helvetica Neue", Arial, sans-serif; + font-family: var( + --clr-sidenav-header-font-family, + Metropolis, + "Avenir Next", + "Helvetica Neue", + Arial, + sans-serif + ); + font-size: 0.7rem; + line-height: 1.2rem; + letter-spacing: normal; +} +.sidenav .nav-group input[type="checkbox"] { + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; + white-space: nowrap; + top: 0; + left: 0; +} +.sidenav .nav-group input[type="checkbox"]:focus + label { + outline: #3b99fc auto 0.25rem; } .sidenav .collapsible label { - padding: 0 0 0 1.3rem; + padding: 0 0 0 1.3rem; } .sidenav .collapsible label:after { - content: ''; - float: left; - height: 0.5rem; - width: 0.5rem; - -webkit-transform: translateX(-0.4rem) translateY(0.35rem); - transform: translateX(-0.4rem) translateY(0.35rem); - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%239a9a9a%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A'); - background-repeat: no-repeat; - background-size: contain; - vertical-align: middle; - margin: 0; + content: ""; + float: left; + height: 0.5rem; + width: 0.5rem; + -webkit-transform: translateX(-0.4rem) translateY(0.35rem); + transform: translateX(-0.4rem) translateY(0.35rem); + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%239a9a9a%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A"); + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; + margin: 0; +} +.sidenav .collapsible input[type="checkbox"]:checked ~ .nav-list, +.sidenav .collapsible input[type="checkbox"]:checked ~ ul { + height: 0; + display: none; +} +.sidenav .collapsible input[type="checkbox"] ~ .nav-list, +.sidenav .collapsible input[type="checkbox"] ~ ul { + height: auto; +} +.sidenav .collapsible input[type="checkbox"]:checked ~ label:after { + -webkit-transform: rotate(-90deg) translateX(-0.35rem) translateY(-0.4rem); + transform: rotate(-90deg) translateX(-0.35rem) translateY(-0.4rem); +} +:root { + --clr-vertical-nav-divider-color: var(--clr-color-neutral-700); + --clr-vertical-nav-icon-active-color: var(--clr-color-action-600); + --clr-vertical-nav-item-color: var(--clr-color-neutral-700); + --clr-vertical-nav-item-active-color: var(--clr-color-neutral-700); + --clr-vertical-nav-bg-color: var(--clr-color-neutral-200); + --clr-vertical-nav-active-bg-color: var(--clr-color-neutral-0); + --clr-vertical-nav-hover-bg-color: var(--clr-color-neutral-400); + --clr-vertical-nav-toggle-icon-color: var(--clr-color-neutral-1000); + --clr-vertical-nav-trigger-divider-border-width: var( + --clr-global-borderwidth + ); + --clr-vertical-nav-trigger-divider-border-color: var(--clr-color-neutral-400); + --clr-vertical-nav-header-font-weight: var(--clr-p4-font-weight); +} +.clr-vertical-nav { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding-top: 0.9rem; + width: 12rem; + min-width: 2.4rem; + background-color: #e8e8e8; + background-color: var(--clr-vertical-nav-bg-color, #e8e8e8); + will-change: width; + -webkit-transition: width 0.2s ease-in-out; + transition: width 0.2s ease-in-out; +} +.clr-vertical-nav .nav-divider { + border-color: #ccc; + border-color: var(--clr-vertical-nav-trigger-divider-border-color, #ccc); + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-vertical-nav-trigger-divider-border-width, 0.05rem); + margin: 0.6rem 0; +} +.clr-vertical-nav .nav-content { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + overflow-y: auto; + overflow-x: hidden; +} +.clr-vertical-nav .nav-group { + display: block; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + height: auto; + min-height: 1.8rem; +} +.clr-vertical-nav .nav-group-content { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + color: #666; + color: var(--clr-vertical-nav-item-color, #666); +} +.clr-vertical-nav .nav-group-content.active { + color: #666; + color: var(--clr-vertical-nav-item-active-color, #666); + background-color: #fff; + background-color: var(--clr-vertical-nav-active-bg-color, #fff); +} +.clr-vertical-nav .nav-group-content.active .nav-icon { + fill: #0072a3; + fill: var(--clr-vertical-nav-icon-active-color, #0072a3); +} +.clr-vertical-nav .nav-group-content:hover { + color: #666; + color: var(--clr-vertical-nav-item-active-color, #666); + background-color: #ccc; + background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); + text-decoration: none; +} +.clr-vertical-nav .nav-group-content .nav-link { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding-left: 0; + min-width: 0; +} +.clr-vertical-nav .nav-group-content .nav-icon { + margin-left: 1.2rem; +} +.clr-vertical-nav .nav-group-content .nav-text { + padding-left: 1.2rem; +} +.clr-vertical-nav .nav-group-content .nav-icon + .nav-text { + padding-left: 0; +} +.clr-vertical-nav .nav-group-content .nav-link + .nav-group-text { + display: none; +} +.clr-vertical-nav .nav-group-trigger, +.clr-vertical-nav .nav-trigger { + -webkit-box-flex: 0; + -ms-flex: 0 0 1.8rem; + flex: 0 0 1.8rem; + border: none; + height: 1.8rem; + padding: 0; + background-color: transparent; + cursor: pointer; + outline-offset: -0.25rem; +} +.clr-vertical-nav .nav-group-trigger cds-icon[shape="angle-double"], +.clr-vertical-nav .nav-trigger cds-icon[shape="angle-double"] { + color: #000; + color: var(--clr-vertical-nav-toggle-icon-color, #000); +} +.clr-vertical-nav .nav-trigger { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + height: 1.8rem; + margin-top: -0.9rem; +} +.clr-vertical-nav .nav-group-trigger { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + color: inherit; + overflow: hidden; + text-align: left; +} +.clr-vertical-nav .nav-group-trigger .nav-group-trigger-icon { + -ms-flex-negative: 0; + flex-shrink: 0; + width: 0.8rem; + height: 1.8rem; + -ms-flex-item-align: center; + align-self: center; + margin-left: 0.5rem; + margin-right: 0.5rem; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.clr-vertical-nav .nav-trigger-icon { + margin-left: auto; + margin-right: 0.5rem; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.clr-vertical-nav .nav-trigger + .nav-content { + border-top-color: #ccc; + border-top-color: var(--clr-vertical-nav-trigger-divider-border-color, #ccc); + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var( + --clr-vertical-nav-trigger-divider-border-width, + 0.05rem + ); + padding-top: 0.6rem; +} +.clr-vertical-nav .nav-group-text, +.clr-vertical-nav .nav-link { + height: 1.8rem; + padding: 0 0.6rem 0 1.2rem; + line-height: 1.8rem; + outline-offset: -0.25rem; +} +.clr-vertical-nav .nav-group-text, +.clr-vertical-nav .nav-text { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.clr-vertical-nav .nav-link { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + color: #666; + color: var(--clr-vertical-nav-item-color, #666); +} +.clr-vertical-nav .nav-link.active { + color: #666; + color: var(--clr-vertical-nav-item-active-color, #666); + background-color: #fff; + background-color: var(--clr-vertical-nav-active-bg-color, #fff); +} +.clr-vertical-nav .nav-link.active .nav-icon { + fill: #0072a3; + fill: var(--clr-vertical-nav-icon-active-color, #0072a3); +} +.clr-vertical-nav .nav-link:hover { + color: #666; + color: var(--clr-vertical-nav-item-active-color, #666); + background-color: #ccc; + background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); + text-decoration: none; +} +.clr-vertical-nav .nav-header { + padding: 0 0.6rem 0 1.2rem; + font-size: 0.6rem; + font-weight: 600; + font-weight: var(--clr-vertical-nav-header-font-weight, 600); + letter-spacing: normal; + line-height: 1.8rem; +} +.clr-vertical-nav .nav-icon { + -webkit-box-flex: 0; + -ms-flex: 0 0 0.8rem; + flex: 0 0 0.8rem; + -ms-flex-item-align: center; + align-self: center; + height: 0.8rem; + width: 0.8rem; + margin-right: 0.3rem; + vertical-align: middle; +} +.clr-vertical-nav clr-vertical-nav-group-children { + display: block; +} +.clr-vertical-nav .nav-btn { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 0; + margin: 0; + background: 0 0; + border: none; + cursor: pointer; + outline-offset: -0.25rem; +} +.clr-vertical-nav .nav-content > .nav-link, +.clr-vertical-nav > .nav-link { + -webkit-box-flex: 0; + -ms-flex: 0 0 1.8rem; + flex: 0 0 1.8rem; +} +.clr-vertical-nav .nav-link + .nav-group-trigger { + -webkit-box-flex: 0; + -ms-flex: 0 0 1.8rem; + flex: 0 0 1.8rem; +} +.clr-vertical-nav .nav-link + .nav-group-trigger .nav-group-text { + display: none; +} +.clr-vertical-nav .nav-icon + .nav-group-text { + padding-left: 0; +} +.clr-vertical-nav.has-nav-groups .nav-group .nav-group-text, +.clr-vertical-nav.has-nav-groups .nav-group .nav-group-trigger, +.clr-vertical-nav.has-nav-groups .nav-link { + font-weight: 600; +} +.clr-vertical-nav.has-nav-groups .nav-group-children .nav-link { + font-weight: 400; +} +.clr-vertical-nav.has-icons .nav-group-children .nav-link { + padding-left: 2.3rem; +} +.clr-vertical-nav .nav-group.active:not(.is-expanded) .nav-group-content { + background-color: #fff; + background-color: var(--clr-vertical-nav-active-bg-color, #fff); +} +.clr-vertical-nav + .nav-group.active:not(.is-expanded) + .nav-group-content + .nav-icon { + fill: #0072a3; + fill: var(--clr-vertical-nav-icon-active-color, #0072a3); +} +.clr-vertical-nav .nav-group-content .nav-link.active ~ .nav-group-trigger { + background-color: #fff; + background-color: var(--clr-vertical-nav-active-bg-color, #fff); +} +.clr-vertical-nav .nav-group-content .nav-link:hover ~ .nav-group-trigger { + background-color: #ccc; + background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); +} +.clr-vertical-nav:not(.is-collapsed) .nav-link + .nav-group-trigger { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed { + width: 2.4rem; + min-width: 2.4rem; + cursor: pointer; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-trigger { + margin-right: 0.15rem; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-icon { + margin: 0; + margin-left: 0.8rem; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-group-content + .nav-link { + -webkit-box-flex: 0; + -ms-flex: 0 0 2.4rem; + flex: 0 0 2.4rem; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-group-content + .nav-link + ~ .nav-group-trigger { + -webkit-box-flex: 0; + -ms-flex: 0 0 0.8rem; + flex: 0 0 0.8rem; + -webkit-transform: translateX(-0.8rem); + transform: translateX(-0.8rem); + pointer-events: none; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-group-trigger, +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-link { + padding: 0; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-group-trigger { + padding-left: 0; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-group-trigger + .nav-group-trigger-icon { + height: 1.8rem; + width: 0.5rem; + margin-left: 0.15rem; + margin-right: 0; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-group, +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed + .nav-link { + display: none; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed.has-icons + .nav-group { + display: block; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed.has-icons + .nav-link { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed.has-icons + .nav-group-text, +.main-container:not([class*="open-overflow-menu"]):not( + [class*="open-hamburger-menu"] + ) + .clr-vertical-nav.is-collapsed.has-icons + .nav-text { + display: none; +} +.clr-vertical-nav.nav-trigger--bottom .nav-trigger { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + margin-top: 0; +} +.clr-vertical-nav.nav-trigger--bottom .nav-trigger + .nav-content { + border-bottom-color: #ccc; + border-bottom-color: var( + --clr-vertical-nav-trigger-divider-border-color, + #ccc + ); + border-bottom-style: solid; + border-bottom-width: 0.05rem; + border-bottom-width: var( + --clr-vertical-nav-trigger-divider-border-width, + 0.05rem + ); + border-top: none; + padding-top: 0; +} +:root { + --clr-sliding-panel-text-color: var(--clr-color-neutral-700); + --clr-nav-background-color: var(--clr-color-neutral-200); + --clr-responsive-nav-hover-bg: var(--clr-color-neutral-0); + --clr-responsive-nav-trigger-bg-color: var(--clr-color-neutral-0); + --clr-responsive-nav-trigger-border-radius: var(--clr-global-borderradius); + --clr-responsive-nav-hamburger-border-radius: var( + --clr-responsive-nav-trigger-border-radius + ); + --clr-responsive-nav-overflow-border-radius: 0.2rem; + --clr-responsive-nav-header-backdrop-bg-color: var(--clr-color-neutral-1000); + --clr-responsive-nav-header-backdrop-opacity: 0.85; +} +.header-hamburger-trigger, +.header-overflow-trigger { + display: none; } -.sidenav .collapsible input[type='checkbox']:checked ~ .nav-list, -.sidenav .collapsible input[type='checkbox']:checked ~ ul { - height: 0; - display: none; +.header-hamburger-trigger > span, +.header-hamburger-trigger > span::after, +.header-hamburger-trigger > span::before { + display: inline-block; + height: 0.1rem; + width: 1.2rem; + background: #fff; + background: var(--clr-responsive-nav-trigger-bg-color, #fff); + border-radius: 0.15rem; + border-radius: var(--clr-responsive-nav-hamburger-border-radius, 0.15rem); } -.sidenav .collapsible input[type='checkbox'] ~ .nav-list, -.sidenav .collapsible input[type='checkbox'] ~ ul { - height: auto; +.header-hamburger-trigger > span { + position: relative; + vertical-align: middle; } -.sidenav .collapsible input[type='checkbox']:checked ~ label:after { - -webkit-transform: rotate(-90deg) translateX(-0.35rem) translateY(-0.4rem); - transform: rotate(-90deg) translateX(-0.35rem) translateY(-0.4rem); +.header-hamburger-trigger > span::after, +.header-hamburger-trigger > span::before { + content: ""; + position: absolute; + left: 0; } -:root { - --clr-vertical-nav-divider-color: var(--clr-color-neutral-700); - --clr-vertical-nav-icon-active-color: var(--clr-color-action-600); - --clr-vertical-nav-item-color: var(--clr-color-neutral-700); - --clr-vertical-nav-item-active-color: var(--clr-color-neutral-700); - --clr-vertical-nav-bg-color: var(--clr-color-neutral-200); - --clr-vertical-nav-active-bg-color: var(--clr-color-neutral-0); - --clr-vertical-nav-hover-bg-color: var(--clr-color-neutral-400); - --clr-vertical-nav-toggle-icon-color: var(--clr-color-neutral-1000); - --clr-vertical-nav-trigger-divider-border-width: var( - --clr-global-borderwidth - ); - --clr-vertical-nav-trigger-divider-border-color: var( - --clr-color-neutral-400 - ); - --clr-vertical-nav-header-font-weight: var(--clr-p4-font-weight); +.header-hamburger-trigger > span::before { + top: -0.35rem; } -.clr-vertical-nav { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - padding-top: 0.9rem; - width: 12rem; - min-width: 2.4rem; - background-color: #e8e8e8; - background-color: var(--clr-vertical-nav-bg-color, #e8e8e8); - will-change: width; - -webkit-transition: width 0.2s ease-in-out; - transition: width 0.2s ease-in-out; +.header-hamburger-trigger > span::after { + bottom: -0.35rem; } -.clr-vertical-nav .nav-divider { - border-color: #ccc; - border-color: var(--clr-vertical-nav-trigger-divider-border-color, #ccc); - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-vertical-nav-trigger-divider-border-width, 0.05rem); - margin: 0.6rem 0; +.header-hamburger-trigger.active > span { + background: 0 0; } -.clr-vertical-nav .nav-content { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - overflow-y: auto; - overflow-x: hidden; +.header-hamburger-trigger.active > span::after, +.header-hamburger-trigger.active > span::before { + left: 0.15rem; + -webkit-transform-origin: 9%; + transform-origin: 9%; + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: + transform 0.6s ease, + -webkit-transform 0.6s ease; } -.clr-vertical-nav .nav-group { - display: block; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - height: auto; - min-height: 1.8rem; +.header-hamburger-trigger.active > span::before { + -webkit-transform: rotate(45deg); + transform: rotate(45deg); } -.clr-vertical-nav .nav-group-content { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - color: #666; - color: var(--clr-vertical-nav-item-color, #666); +.header-hamburger-trigger.active > span::after { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); } -.clr-vertical-nav .nav-group-content.active { - color: #666; - color: var(--clr-vertical-nav-item-active-color, #666); - background-color: #fff; - background-color: var(--clr-vertical-nav-active-bg-color, #fff); +.header-overflow-trigger > span, +.header-overflow-trigger > span::after, +.header-overflow-trigger > span::before { + display: inline-block; + height: 0.2rem; + width: 0.2rem; + background: #fff; + background: var(--clr-responsive-nav-trigger-bg-color, #fff); + border-radius: 0.2rem; + border-radius: var(--clr-responsive-nav-overflow-border-radius, 0.2rem); } -.clr-vertical-nav .nav-group-content.active .nav-icon { - fill: #0072a3; - fill: var(--clr-vertical-nav-icon-active-color, #0072a3); +.header-overflow-trigger > span { + position: relative; + vertical-align: middle; } -.clr-vertical-nav .nav-group-content:hover { - color: #666; - color: var(--clr-vertical-nav-item-active-color, #666); - background-color: #ccc; - background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); - text-decoration: none; +.header-overflow-trigger > span::after, +.header-overflow-trigger > span::before { + content: ""; + position: absolute; + left: 0; } -.clr-vertical-nav .nav-group-content .nav-link { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - padding-left: 0; - min-width: 0; +.header-overflow-trigger > span::before { + top: -0.4rem; } -.clr-vertical-nav .nav-group-content .nav-icon { - margin-left: 1.2rem; +.header-overflow-trigger > span::after { + bottom: -0.4rem; } -.clr-vertical-nav .nav-group-content .nav-text { - padding-left: 1.2rem; +.header-overflow-trigger.active > span { + background: 0 0; } -.clr-vertical-nav .nav-group-content .nav-icon + .nav-text { - padding-left: 0; +.header-overflow-trigger.active > span::after, +.header-overflow-trigger.active > span::before { + height: 0.1rem; + width: 1.2rem; + left: -0.3rem; + -webkit-transform-origin: -3%; + transform-origin: -3%; + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: + transform 0.6s ease, + -webkit-transform 0.6s ease; } -.clr-vertical-nav .nav-group-content .nav-link + .nav-group-text { - display: none; +.header-overflow-trigger.active > span::before { + -webkit-transform: rotate(45deg); + transform: rotate(45deg); } -.clr-vertical-nav .nav-group-trigger, -.clr-vertical-nav .nav-trigger { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.8rem; - flex: 0 0 1.8rem; +.header-overflow-trigger.active > span::after { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} +@media screen and (max-width: 768px) { + .main-container .header-hamburger-trigger, + .main-container .header-overflow-trigger { + display: inline-block; border: none; - height: 1.8rem; - padding: 0; - background-color: transparent; + background: 0 0; cursor: pointer; + font-size: 1.2rem; + height: 3rem; + width: 3rem; + padding: 0 0 0.2rem 0; + text-align: center; + white-space: nowrap; + color: #fafafa; + color: var(--clr-header-font-color, #fafafa); + opacity: 0.65; + opacity: var(--clr-header-nav-opacity, 0.65); + } + .main-container .header-hamburger-trigger:focus, + .main-container .header-overflow-trigger:focus { outline-offset: -0.25rem; -} -.clr-vertical-nav .nav-group-trigger cds-icon[shape='angle-double'], -.clr-vertical-nav .nav-trigger cds-icon[shape='angle-double'] { - color: #000; - color: var(--clr-vertical-nav-toggle-icon-color, #000); -} -.clr-vertical-nav .nav-trigger { + } + .main-container .header-hamburger-trigger:enabled:hover, + .main-container .header-overflow-trigger:enabled:hover { + opacity: 1; + opacity: var(--clr-header-nav-hover-opacity, 1); + } + .main-container .header-hamburger-trigger:disabled, + .main-container .header-overflow-trigger:disabled { + cursor: not-allowed; + } + .main-container .clr-vertical-nav.clr-nav-level-1, + .main-container .header-nav.clr-nav-level-1, + .main-container .sidenav.clr-nav-level-1, + .main-container .sub-nav.clr-nav-level-1, + .main-container .subnav.clr-nav-level-1 { display: -webkit-box; display: -ms-flexbox; display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - height: 1.8rem; - margin-top: -0.9rem; -} -.clr-vertical-nav .nav-group-trigger { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + position: fixed; + top: 0; + right: auto; + bottom: 0; + left: 0; + background: #e8e8e8; + background: var(--clr-nav-background-color, #e8e8e8); + z-index: 1039; + height: 100vh; + -webkit-transform: translateX(-18rem); + transform: translateX(-18rem); + -webkit-transition: -webkit-transform 0.3s ease; + transition: -webkit-transform 0.3s ease; + transition: transform 0.3s ease; + transition: + transform 0.3s ease, + -webkit-transform 0.3s ease; + } + .main-container .clr-vertical-nav.clr-nav-level-2, + .main-container .header-nav.clr-nav-level-2, + .main-container .sidenav.clr-nav-level-2, + .main-container .sub-nav.clr-nav-level-2, + .main-container .subnav.clr-nav-level-2 { display: -webkit-box; display: -ms-flexbox; display: flex; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - color: inherit; - overflow: hidden; - text-align: left; -} -.clr-vertical-nav .nav-group-trigger .nav-group-trigger-icon { - -ms-flex-negative: 0; - flex-shrink: 0; - width: 0.8rem; - height: 1.8rem; - -ms-flex-item-align: center; - align-self: center; - margin-left: 0.5rem; - margin-right: 0.5rem; - -webkit-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; -} -.clr-vertical-nav .nav-trigger-icon { - margin-left: auto; - margin-right: 0.5rem; - -webkit-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; -} -.clr-vertical-nav .nav-trigger + .nav-content { - border-top-color: #ccc; - border-top-color: var( - --clr-vertical-nav-trigger-divider-border-color, - #ccc - ); - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var( - --clr-vertical-nav-trigger-divider-border-width, - 0.05rem - ); - padding-top: 0.6rem; -} -.clr-vertical-nav .nav-group-text, -.clr-vertical-nav .nav-link { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: auto; + background: #e8e8e8; + background: var(--clr-nav-background-color, #e8e8e8); + z-index: 1039; + height: 100vh; + -webkit-transform: translateX(18rem); + transform: translateX(18rem); + -webkit-transition: -webkit-transform 0.3s ease; + transition: -webkit-transform 0.3s ease; + transition: transform 0.3s ease; + transition: + transform 0.3s ease, + -webkit-transform 0.3s ease; + } + .main-container .sub-nav.clr-nav-level-1 .nav, + .main-container .sub-nav.clr-nav-level-1 aside, + .main-container .sub-nav.clr-nav-level-2 .nav, + .main-container .sub-nav.clr-nav-level-2 aside, + .main-container .subnav.clr-nav-level-1 .nav, + .main-container .subnav.clr-nav-level-1 aside, + .main-container .subnav.clr-nav-level-2 .nav, + .main-container .subnav.clr-nav-level-2 aside { + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + } + .main-container .sub-nav.clr-nav-level-1 aside, + .main-container .sub-nav.clr-nav-level-2 aside, + .main-container .subnav.clr-nav-level-1 aside, + .main-container .subnav.clr-nav-level-2 aside { + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; + } + .main-container .sub-nav.clr-nav-level-1 .nav, + .main-container .sub-nav.clr-nav-level-2 .nav, + .main-container .subnav.clr-nav-level-1 .nav, + .main-container .subnav.clr-nav-level-2 .nav { + padding-left: 0; + } + .main-container .sub-nav.clr-nav-level-1 .nav .nav-item, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-item, + .main-container .subnav.clr-nav-level-1 .nav .nav-item, + .main-container .subnav.clr-nav-level-2 .nav .nav-item { height: 1.8rem; + margin-right: 0; + } + .main-container .sub-nav.clr-nav-level-1 .nav .nav-link, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-link, + .main-container .subnav.clr-nav-level-1 .nav .nav-link, + .main-container .subnav.clr-nav-level-2 .nav .nav-link { padding: 0 0.6rem 0 1.2rem; - line-height: 1.8rem; - outline-offset: -0.25rem; -} -.clr-vertical-nav .nav-group-text, -.clr-vertical-nav .nav-text { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - white-space: nowrap; + width: 100%; + max-width: 100%; overflow: hidden; text-overflow: ellipsis; -} -.clr-vertical-nav .nav-link { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + border-radius: 0; + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-global-borderradius, 0.15rem); + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-global-borderradius, 0.15rem); color: #666; color: var(--clr-vertical-nav-item-color, #666); -} -.clr-vertical-nav .nav-link.active { + } + .main-container .sub-nav.clr-nav-level-1 .nav .nav-link.active, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-link.active, + .main-container .subnav.clr-nav-level-1 .nav .nav-link.active, + .main-container .subnav.clr-nav-level-2 .nav .nav-link.active { color: #666; color: var(--clr-vertical-nav-item-active-color, #666); background-color: #fff; background-color: var(--clr-vertical-nav-active-bg-color, #fff); -} -.clr-vertical-nav .nav-link.active .nav-icon { + } + .main-container .sub-nav.clr-nav-level-1 .nav .nav-link.active .nav-icon, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-link.active .nav-icon, + .main-container .subnav.clr-nav-level-1 .nav .nav-link.active .nav-icon, + .main-container .subnav.clr-nav-level-2 .nav .nav-link.active .nav-icon { fill: #0072a3; fill: var(--clr-vertical-nav-icon-active-color, #0072a3); -} -.clr-vertical-nav .nav-link:hover { + } + .main-container .sub-nav.clr-nav-level-1 .nav .nav-link:hover, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-link:hover, + .main-container .subnav.clr-nav-level-1 .nav .nav-link:hover, + .main-container .subnav.clr-nav-level-2 .nav .nav-link:hover { color: #666; color: var(--clr-vertical-nav-item-active-color, #666); background-color: #ccc; background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); text-decoration: none; -} -.clr-vertical-nav .nav-header { - padding: 0 0.6rem 0 1.2rem; - font-size: 0.6rem; - font-weight: 600; - font-weight: var(--clr-vertical-nav-header-font-weight, 600); - letter-spacing: normal; - line-height: 1.8rem; -} -.clr-vertical-nav .nav-icon { - -webkit-box-flex: 0; - -ms-flex: 0 0 0.8rem; - flex: 0 0 0.8rem; - -ms-flex-item-align: center; - align-self: center; - height: 0.8rem; - width: 0.8rem; - margin-right: 0.3rem; - vertical-align: middle; -} -.clr-vertical-nav clr-vertical-nav-group-children { - display: block; -} -.clr-vertical-nav .nav-btn { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - padding: 0; - margin: 0; - background: 0 0; - border: none; + } + .main-container .sub-nav.clr-nav-level-1 .nav .nav-link.active, + .main-container .sub-nav.clr-nav-level-1 .nav .nav-link:hover, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-link.active, + .main-container .sub-nav.clr-nav-level-2 .nav .nav-link:hover, + .main-container .subnav.clr-nav-level-1 .nav .nav-link.active, + .main-container .subnav.clr-nav-level-1 .nav .nav-link:hover, + .main-container .subnav.clr-nav-level-2 .nav .nav-link.active, + .main-container .subnav.clr-nav-level-2 .nav .nav-link:hover { + -webkit-box-shadow: none; + box-shadow: none; + } + .main-container .sidenav.clr-nav-level-1 .nav-link.active, + .main-container .sidenav.clr-nav-level-1 .nav-link:hover, + .main-container .sidenav.clr-nav-level-2 .nav-link.active, + .main-container .sidenav.clr-nav-level-2 .nav-link:hover { + color: inherit; + background: #fff; + background: var(--clr-responsive-nav-hover-bg, #fff); + } + .main-container .clr-vertical-nav.clr-nav-level-1, + .main-container .clr-vertical-nav.clr-nav-level-2, + .main-container .sidenav.clr-nav-level-1, + .main-container .sidenav.clr-nav-level-2 { + border-right: none; + } + .main-container .header-overflow-trigger { + position: relative; + } + .main-container .header-overflow-trigger::after { + position: absolute; + content: ""; + display: inline-block; + position: absolute; + content: ""; + background: #fafafa; + background: var(--clr-header-font-color, #fafafa); + opacity: 0.15; + opacity: var(--clr-header-divider-opacity, 0.15); + opacity: 0.15; + height: 2rem; + width: 0.05rem; + width: var(--clr-global-borderwidth, 0.05rem); + top: 0.5rem; + left: 0; + left: 0; + } + .main-container .header .branding { + max-width: 12rem; + min-width: 0; + overflow: hidden; + } + .main-container .header .header-hamburger-trigger + .branding { + padding-left: 0; + } + .main-container .header .header-hamburger-trigger + .branding .clr-icon, + .main-container .header .header-hamburger-trigger + .branding .logo, + .main-container .header .header-hamburger-trigger + .branding cds-icon { + display: none; + } + .main-container .header .branding + .header-overflow-trigger, + .main-container .header .header-nav + .header-overflow-trigger { + margin-left: auto; + } + .main-container.open-hamburger-menu .header .header-backdrop, + .main-container.open-overflow-menu .header .header-backdrop { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: #000; + background: var(--clr-responsive-nav-header-backdrop-bg-color, #000); + opacity: 0.85; + opacity: var(--clr-responsive-nav-header-backdrop-opacity, 0.85); cursor: pointer; - outline-offset: -0.25rem; -} -.clr-vertical-nav .nav-content > .nav-link, -.clr-vertical-nav > .nav-link { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.8rem; - flex: 0 0 1.8rem; -} -.clr-vertical-nav .nav-link + .nav-group-trigger { + z-index: 1038; + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link { -webkit-box-flex: 0; - -ms-flex: 0 0 1.8rem; - flex: 0 0 1.8rem; -} -.clr-vertical-nav .nav-link + .nav-group-trigger .nav-group-text { + -ms-flex: 0 0 auto; + flex: 0 0 auto; + opacity: 1; + color: #666; + color: var(--clr-vertical-nav-item-color, #666); + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .fa, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .nav-icon, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .fa, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .nav-icon, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .fa, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .nav-icon, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .fa, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .nav-icon { display: none; -} -.clr-vertical-nav .nav-icon + .nav-group-text { - padding-left: 0; -} -.clr-vertical-nav.has-nav-groups .nav-group .nav-group-text, -.clr-vertical-nav.has-nav-groups .nav-group .nav-group-trigger, -.clr-vertical-nav.has-nav-groups .nav-link { - font-weight: 600; -} -.clr-vertical-nav.has-nav-groups .nav-group-children .nav-link { + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .nav-text, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .nav-text, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .nav-text, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .nav-text { + display: inline-block; + color: #666; + color: var(--clr-sliding-panel-text-color, #666); + line-height: 1.2rem; + padding: 0.3rem 0 0.3rem 1.2rem; + white-space: normal; font-weight: 400; -} -.clr-vertical-nav.has-icons .nav-group-children .nav-link { - padding-left: 2.3rem; -} -.clr-vertical-nav .nav-group.active:not(.is-expanded) .nav-group-content { + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .nav-icon + + .nav-text, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .nav-icon + + .nav-text, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link + .nav-icon + + .nav-text, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link + .nav-icon + + .nav-text { + display: inline-block; + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link.active, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link.active, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link.active, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link.active { + color: #666; + color: var(--clr-vertical-nav-item-active-color, #666); background-color: #fff; background-color: var(--clr-vertical-nav-active-bg-color, #fff); -} -.clr-vertical-nav - .nav-group.active:not(.is-expanded) - .nav-group-content + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link.active + .nav-icon, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link.active + .nav-icon, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link.active + .nav-icon, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link.active .nav-icon { fill: #0072a3; fill: var(--clr-vertical-nav-icon-active-color, #0072a3); -} -.clr-vertical-nav .nav-group-content .nav-link.active ~ .nav-group-trigger { - background-color: #fff; - background-color: var(--clr-vertical-nav-active-bg-color, #fff); -} -.clr-vertical-nav .nav-group-content .nav-link:hover ~ .nav-group-trigger { + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link:hover, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link:hover, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link:hover, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link:hover { + color: #666; + color: var(--clr-vertical-nav-item-active-color, #666); background-color: #ccc; background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); -} -.clr-vertical-nav:not(.is-collapsed) .nav-link + .nav-group-trigger { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed { - width: 2.4rem; - min-width: 2.4rem; - cursor: pointer; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-trigger { - margin-right: 0.15rem; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-icon { - margin: 0; - margin-left: 0.8rem; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-group-content - .nav-link { - -webkit-box-flex: 0; - -ms-flex: 0 0 2.4rem; - flex: 0 0 2.4rem; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-group-content - .nav-link - ~ .nav-group-trigger { - -webkit-box-flex: 0; - -ms-flex: 0 0 0.8rem; - flex: 0 0 0.8rem; - -webkit-transform: translateX(-0.8rem); - transform: translateX(-0.8rem); - pointer-events: none; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-group-trigger, -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-link { - padding: 0; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-group-trigger { - padding-left: 0; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-group-trigger - .nav-group-trigger-icon { - height: 1.8rem; - width: 0.5rem; - margin-left: 0.15rem; - margin-right: 0; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-group, -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed - .nav-link { - display: none; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed.has-icons - .nav-group { - display: block; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed.has-icons - .nav-link { - display: -webkit-box; - display: -ms-flexbox; - display: flex; -} -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed.has-icons - .nav-group-text, -.main-container:not([class*='open-overflow-menu']):not( - [class*='open-hamburger-menu'] - ) - .clr-vertical-nav.is-collapsed.has-icons - .nav-text { - display: none; -} -.clr-vertical-nav.nav-trigger--bottom .nav-trigger { - -webkit-box-ordinal-group: 3; - -ms-flex-order: 2; - order: 2; - margin-top: 0; -} -.clr-vertical-nav.nav-trigger--bottom .nav-trigger + .nav-content { - border-bottom-color: #ccc; - border-bottom-color: var( - --clr-vertical-nav-trigger-divider-border-color, - #ccc - ); - border-bottom-style: solid; - border-bottom-width: 0.05rem; - border-bottom-width: var( - --clr-vertical-nav-trigger-divider-border-width, - 0.05rem - ); - border-top: none; - padding-top: 0; -} -:root { - --clr-sliding-panel-text-color: var(--clr-color-neutral-700); - --clr-nav-background-color: var(--clr-color-neutral-200); - --clr-responsive-nav-hover-bg: var(--clr-color-neutral-0); - --clr-responsive-nav-trigger-bg-color: var(--clr-color-neutral-0); - --clr-responsive-nav-trigger-border-radius: var(--clr-global-borderradius); - --clr-responsive-nav-hamburger-border-radius: var( - --clr-responsive-nav-trigger-border-radius - ); - --clr-responsive-nav-overflow-border-radius: 0.2rem; - --clr-responsive-nav-header-backdrop-bg-color: var( - --clr-color-neutral-1000 - ); - --clr-responsive-nav-header-backdrop-opacity: 0.85; -} -.header-hamburger-trigger, -.header-overflow-trigger { + text-decoration: none; + } + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-1 + .nav-link.active + > .nav-text, + .main-container.open-hamburger-menu + .header + .header-nav.clr-nav-level-2 + .nav-link.active + > .nav-text, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-1 + .nav-link.active + > .nav-text, + .main-container.open-overflow-menu + .header + .header-nav.clr-nav-level-2 + .nav-link.active + > .nav-text { + color: inherit; + } + .main-container.open-hamburger-menu .clr-vertical-nav .nav-trigger, + .main-container.open-overflow-menu .clr-vertical-nav .nav-trigger { display: none; -} -.header-hamburger-trigger > span, -.header-hamburger-trigger > span::after, -.header-hamburger-trigger > span::before { + } + .main-container.open-hamburger-menu .header .branding { + position: fixed; + top: 0; + left: 0; + overflow: hidden; + width: 18rem; + max-width: 18rem; + z-index: 1040; + padding-left: 1.2rem; + } + .main-container.open-hamburger-menu .header .branding > .nav-link { + overflow: hidden; + } + .main-container.open-hamburger-menu .header .branding .clr-icon, + .main-container.open-hamburger-menu .header .branding .logo, + .main-container.open-hamburger-menu .header .branding cds-icon { display: inline-block; - height: 0.1rem; - width: 1.2rem; - background: #fff; - background: var(--clr-responsive-nav-trigger-bg-color, #fff); + } + .main-container.open-hamburger-menu .header .branding .clr-vmw-logo, + .main-container.open-hamburger-menu + .header + .branding + cds-icon[shape="vm-bug"] { + background-color: #8c8c8c; border-radius: 0.15rem; - border-radius: var(--clr-responsive-nav-hamburger-border-radius, 0.15rem); -} -.header-hamburger-trigger > span { - position: relative; - vertical-align: middle; -} -.header-hamburger-trigger > span::after, -.header-hamburger-trigger > span::before { - content: ''; - position: absolute; + } + .main-container.open-hamburger-menu .header .branding .title { + color: #666; + color: var(--clr-sliding-panel-text-color, #666); + text-overflow: ellipsis; + overflow: hidden; + } + .main-container.open-hamburger-menu .header-hamburger-trigger { + position: fixed; + top: 0; + right: auto; left: 0; -} -.header-hamburger-trigger > span::before { - top: -0.35rem; -} -.header-hamburger-trigger > span::after { - bottom: -0.35rem; -} -.header-hamburger-trigger.active > span { + z-index: 1039; + -webkit-transform: translateX(18.6rem); + transform: translateX(18.6rem); + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: + transform 0.6s ease, + -webkit-transform 0.6s ease; + } + .main-container.open-hamburger-menu .header-hamburger-trigger::after { + content: none; + } + .main-container.open-hamburger-menu .header-hamburger-trigger > span { background: 0 0; -} -.header-hamburger-trigger.active > span::after, -.header-hamburger-trigger.active > span::before { + } + .main-container.open-hamburger-menu .header-hamburger-trigger > span::after, + .main-container.open-hamburger-menu .header-hamburger-trigger > span::before { left: 0.15rem; -webkit-transform-origin: 9%; transform-origin: 9%; @@ -9307,49 +9849,85 @@ a.link-visited:link { transition: -webkit-transform 0.6s ease; transition: transform 0.6s ease; transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; -} -.header-hamburger-trigger.active > span::before { + transform 0.6s ease, + -webkit-transform 0.6s ease; + } + .main-container.open-hamburger-menu .header-hamburger-trigger > span::before { -webkit-transform: rotate(45deg); transform: rotate(45deg); -} -.header-hamburger-trigger.active > span::after { + } + .main-container.open-hamburger-menu .header-hamburger-trigger > span::after { -webkit-transform: rotate(-45deg); transform: rotate(-45deg); -} -.header-overflow-trigger > span, -.header-overflow-trigger > span::after, -.header-overflow-trigger > span::before { - display: inline-block; - height: 0.2rem; - width: 0.2rem; - background: #fff; - background: var(--clr-responsive-nav-trigger-bg-color, #fff); - border-radius: 0.2rem; - border-radius: var(--clr-responsive-nav-overflow-border-radius, 0.2rem); -} -.header-overflow-trigger > span { - position: relative; - vertical-align: middle; -} -.header-overflow-trigger > span::after, -.header-overflow-trigger > span::before { - content: ''; - position: absolute; - left: 0; -} -.header-overflow-trigger > span::before { - top: -0.4rem; -} -.header-overflow-trigger > span::after { - bottom: -0.4rem; -} -.header-overflow-trigger.active > span { + } + .main-container.open-hamburger-menu .clr-vertical-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .header-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .sidenav.clr-nav-level-1, + .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .subnav.clr-nav-level-1 { + padding-top: 4.2rem; + -webkit-transform: translateX(0); + transform: translateX(0); + -webkit-transition: -webkit-transform 0.3s ease; + transition: -webkit-transform 0.3s ease; + transition: transform 0.3s ease; + transition: + transform 0.3s ease, + -webkit-transform 0.3s ease; + } + .main-container.open-hamburger-menu + .clr-vertical-nav.clr-nav-level-1 + .sidenav-content, + .main-container.open-hamburger-menu + .header-nav.clr-nav-level-1 + .sidenav-content, + .main-container.open-hamburger-menu .sidenav.clr-nav-level-1 .sidenav-content, + .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1 .sidenav-content, + .main-container.open-hamburger-menu .subnav.clr-nav-level-1 .sidenav-content { + padding-bottom: 1.2rem; + } + .main-container.open-overflow-menu .clr-vertical-nav.clr-nav-level-2, + .main-container.open-overflow-menu .header-nav.clr-nav-level-2, + .main-container.open-overflow-menu .sidenav.clr-nav-level-2, + .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, + .main-container.open-overflow-menu .subnav.clr-nav-level-2 { + -webkit-transform: translateX(0); + transform: translateX(0); + -webkit-transition: -webkit-transform 0.3s ease; + transition: -webkit-transform 0.3s ease; + transition: transform 0.3s ease; + transition: + transform 0.3s ease, + -webkit-transform 0.3s ease; + } + .main-container.open-overflow-menu .header-nav.clr-nav-level-2, + .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, + .main-container.open-overflow-menu .subnav.clr-nav-level-2 { + padding-top: 1.2rem; + } + .main-container.open-overflow-menu .header-overflow-trigger { + position: fixed; + top: 0; + right: 0; + left: auto; + z-index: 1039; + -webkit-transform: translateX(-18.6rem); + transform: translateX(-18.6rem); + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: + transform 0.6s ease, + -webkit-transform 0.6s ease; + } + .main-container.open-overflow-menu .header-overflow-trigger::after { + content: none; + } + .main-container.open-overflow-menu .header-overflow-trigger > span { background: 0 0; -} -.header-overflow-trigger.active > span::after, -.header-overflow-trigger.active > span::before { + } + .main-container.open-overflow-menu .header-overflow-trigger > span::after, + .main-container.open-overflow-menu .header-overflow-trigger > span::before { height: 0.1rem; width: 1.2rem; left: -0.3rem; @@ -9359,3419 +9937,2733 @@ a.link-visited:link { transition: -webkit-transform 0.6s ease; transition: transform 0.6s ease; transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; -} -.header-overflow-trigger.active > span::before { + transform 0.6s ease, + -webkit-transform 0.6s ease; + } + .main-container.open-overflow-menu .header-overflow-trigger > span::before { -webkit-transform: rotate(45deg); transform: rotate(45deg); -} -.header-overflow-trigger.active > span::after { + } + .main-container.open-overflow-menu .header-overflow-trigger > span::after { -webkit-transform: rotate(-45deg); transform: rotate(-45deg); -} -@media screen and (max-width: 768px) { - .main-container .header-hamburger-trigger, - .main-container .header-overflow-trigger { - display: inline-block; - border: none; - background: 0 0; - cursor: pointer; - font-size: 1.2rem; - height: 3rem; - width: 3rem; - padding: 0 0 0.2rem 0; - text-align: center; - white-space: nowrap; - color: #fafafa; - color: var(--clr-header-font-color, #fafafa); - opacity: 0.65; - opacity: var(--clr-header-nav-opacity, 0.65); - } - .main-container .header-hamburger-trigger:focus, - .main-container .header-overflow-trigger:focus { - outline-offset: -0.25rem; - } - .main-container .header-hamburger-trigger:enabled:hover, - .main-container .header-overflow-trigger:enabled:hover { - opacity: 1; - opacity: var(--clr-header-nav-hover-opacity, 1); - } - .main-container .header-hamburger-trigger:disabled, - .main-container .header-overflow-trigger:disabled { - cursor: not-allowed; - } - .main-container .clr-vertical-nav.clr-nav-level-1, - .main-container .header-nav.clr-nav-level-1, - .main-container .sidenav.clr-nav-level-1, - .main-container .sub-nav.clr-nav-level-1, - .main-container .subnav.clr-nav-level-1 { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - position: fixed; - top: 0; - right: auto; - bottom: 0; - left: 0; - background: #e8e8e8; - background: var(--clr-nav-background-color, #e8e8e8); - z-index: 1039; - height: 100vh; - -webkit-transform: translateX(-18rem); - transform: translateX(-18rem); - -webkit-transition: -webkit-transform 0.3s ease; - transition: -webkit-transform 0.3s ease; - transition: transform 0.3s ease; - transition: - transform 0.3s ease, - -webkit-transform 0.3s ease; - } - .main-container .clr-vertical-nav.clr-nav-level-2, - .main-container .header-nav.clr-nav-level-2, - .main-container .sidenav.clr-nav-level-2, - .main-container .sub-nav.clr-nav-level-2, - .main-container .subnav.clr-nav-level-2 { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: auto; - background: #e8e8e8; - background: var(--clr-nav-background-color, #e8e8e8); - z-index: 1039; - height: 100vh; - -webkit-transform: translateX(18rem); - transform: translateX(18rem); - -webkit-transition: -webkit-transform 0.3s ease; - transition: -webkit-transform 0.3s ease; - transition: transform 0.3s ease; - transition: - transform 0.3s ease, - -webkit-transform 0.3s ease; - } - .main-container .sub-nav.clr-nav-level-1 .nav, - .main-container .sub-nav.clr-nav-level-1 aside, - .main-container .sub-nav.clr-nav-level-2 .nav, - .main-container .sub-nav.clr-nav-level-2 aside, - .main-container .subnav.clr-nav-level-1 .nav, - .main-container .subnav.clr-nav-level-1 aside, - .main-container .subnav.clr-nav-level-2 .nav, - .main-container .subnav.clr-nav-level-2 aside { - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - } - .main-container .sub-nav.clr-nav-level-1 aside, - .main-container .sub-nav.clr-nav-level-2 aside, - .main-container .subnav.clr-nav-level-1 aside, - .main-container .subnav.clr-nav-level-2 aside { - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - width: 100%; - } - .main-container .sub-nav.clr-nav-level-1 .nav, - .main-container .sub-nav.clr-nav-level-2 .nav, - .main-container .subnav.clr-nav-level-1 .nav, - .main-container .subnav.clr-nav-level-2 .nav { - padding-left: 0; - } - .main-container .sub-nav.clr-nav-level-1 .nav .nav-item, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-item, - .main-container .subnav.clr-nav-level-1 .nav .nav-item, - .main-container .subnav.clr-nav-level-2 .nav .nav-item { - height: 1.8rem; - margin-right: 0; - } - .main-container .sub-nav.clr-nav-level-1 .nav .nav-link, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-link, - .main-container .subnav.clr-nav-level-1 .nav .nav-link, - .main-container .subnav.clr-nav-level-2 .nav .nav-link { - padding: 0 0.6rem 0 1.2rem; - width: 100%; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - border-radius: 0; - border-top-left-radius: 0.15rem; - border-top-left-radius: var(--clr-global-borderradius, 0.15rem); - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-global-borderradius, 0.15rem); - color: #666; - color: var(--clr-vertical-nav-item-color, #666); - } - .main-container .sub-nav.clr-nav-level-1 .nav .nav-link.active, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-link.active, - .main-container .subnav.clr-nav-level-1 .nav .nav-link.active, - .main-container .subnav.clr-nav-level-2 .nav .nav-link.active { - color: #666; - color: var(--clr-vertical-nav-item-active-color, #666); - background-color: #fff; - background-color: var(--clr-vertical-nav-active-bg-color, #fff); - } - .main-container .sub-nav.clr-nav-level-1 .nav .nav-link.active .nav-icon, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-link.active .nav-icon, - .main-container .subnav.clr-nav-level-1 .nav .nav-link.active .nav-icon, - .main-container .subnav.clr-nav-level-2 .nav .nav-link.active .nav-icon { - fill: #0072a3; - fill: var(--clr-vertical-nav-icon-active-color, #0072a3); - } - .main-container .sub-nav.clr-nav-level-1 .nav .nav-link:hover, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-link:hover, - .main-container .subnav.clr-nav-level-1 .nav .nav-link:hover, - .main-container .subnav.clr-nav-level-2 .nav .nav-link:hover { - color: #666; - color: var(--clr-vertical-nav-item-active-color, #666); - background-color: #ccc; - background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); - text-decoration: none; - } - .main-container .sub-nav.clr-nav-level-1 .nav .nav-link.active, - .main-container .sub-nav.clr-nav-level-1 .nav .nav-link:hover, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-link.active, - .main-container .sub-nav.clr-nav-level-2 .nav .nav-link:hover, - .main-container .subnav.clr-nav-level-1 .nav .nav-link.active, - .main-container .subnav.clr-nav-level-1 .nav .nav-link:hover, - .main-container .subnav.clr-nav-level-2 .nav .nav-link.active, - .main-container .subnav.clr-nav-level-2 .nav .nav-link:hover { - -webkit-box-shadow: none; - box-shadow: none; - } - .main-container .sidenav.clr-nav-level-1 .nav-link.active, - .main-container .sidenav.clr-nav-level-1 .nav-link:hover, - .main-container .sidenav.clr-nav-level-2 .nav-link.active, - .main-container .sidenav.clr-nav-level-2 .nav-link:hover { - color: inherit; - background: #fff; - background: var(--clr-responsive-nav-hover-bg, #fff); - } - .main-container .clr-vertical-nav.clr-nav-level-1, - .main-container .clr-vertical-nav.clr-nav-level-2, - .main-container .sidenav.clr-nav-level-1, - .main-container .sidenav.clr-nav-level-2 { - border-right: none; - } - .main-container .header-overflow-trigger { - position: relative; - } - .main-container .header-overflow-trigger::after { - position: absolute; - content: ''; - display: inline-block; - position: absolute; - content: ''; - background: #fafafa; - background: var(--clr-header-font-color, #fafafa); - opacity: 0.15; - opacity: var(--clr-header-divider-opacity, 0.15); - opacity: 0.15; - height: 2rem; - width: 0.05rem; - width: var(--clr-global-borderwidth, 0.05rem); - top: 0.5rem; - left: 0; - left: 0; - } - .main-container .header .branding { - max-width: 12rem; - min-width: 0; - overflow: hidden; - } - .main-container .header .header-hamburger-trigger + .branding { - padding-left: 0; - } - .main-container .header .header-hamburger-trigger + .branding .clr-icon, - .main-container .header .header-hamburger-trigger + .branding .logo, - .main-container .header .header-hamburger-trigger + .branding cds-icon { - display: none; - } - .main-container .header .branding + .header-overflow-trigger, - .main-container .header .header-nav + .header-overflow-trigger { - margin-left: auto; - } - .main-container.open-hamburger-menu .header .header-backdrop, - .main-container.open-overflow-menu .header .header-backdrop { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: #000; - background: var(--clr-responsive-nav-header-backdrop-bg-color, #000); - opacity: 0.85; - opacity: var(--clr-responsive-nav-header-backdrop-opacity, 0.85); - cursor: pointer; - z-index: 1038; - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - opacity: 1; - color: #666; - color: var(--clr-vertical-nav-item-color, #666); - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .fa, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .nav-icon, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .fa, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .nav-icon, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .fa, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .nav-icon, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .fa, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .nav-icon { - display: none; - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .nav-text, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .nav-text, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .nav-text, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .nav-text { - display: inline-block; - color: #666; - color: var(--clr-sliding-panel-text-color, #666); - line-height: 1.2rem; - padding: 0.3rem 0 0.3rem 1.2rem; - white-space: normal; - font-weight: 400; - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .nav-icon - + .nav-text, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .nav-icon - + .nav-text, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link - .nav-icon - + .nav-text, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link - .nav-icon - + .nav-text { - display: inline-block; - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link.active, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link.active, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link.active, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link.active { - color: #666; - color: var(--clr-vertical-nav-item-active-color, #666); - background-color: #fff; - background-color: var(--clr-vertical-nav-active-bg-color, #fff); - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link.active - .nav-icon, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link.active - .nav-icon, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link.active - .nav-icon, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link.active - .nav-icon { - fill: #0072a3; - fill: var(--clr-vertical-nav-icon-active-color, #0072a3); - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link:hover, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link:hover, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link:hover, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link:hover { - color: #666; - color: var(--clr-vertical-nav-item-active-color, #666); - background-color: #ccc; - background-color: var(--clr-vertical-nav-hover-bg-color, #ccc); - text-decoration: none; - } - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-1 - .nav-link.active - > .nav-text, - .main-container.open-hamburger-menu - .header - .header-nav.clr-nav-level-2 - .nav-link.active - > .nav-text, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-1 - .nav-link.active - > .nav-text, - .main-container.open-overflow-menu - .header - .header-nav.clr-nav-level-2 - .nav-link.active - > .nav-text { - color: inherit; - } - .main-container.open-hamburger-menu .clr-vertical-nav .nav-trigger, - .main-container.open-overflow-menu .clr-vertical-nav .nav-trigger { - display: none; - } - .main-container.open-hamburger-menu .header .branding { - position: fixed; - top: 0; - left: 0; - overflow: hidden; - width: 18rem; - max-width: 18rem; - z-index: 1040; - padding-left: 1.2rem; - } - .main-container.open-hamburger-menu .header .branding > .nav-link { - overflow: hidden; - } - .main-container.open-hamburger-menu .header .branding .clr-icon, - .main-container.open-hamburger-menu .header .branding .logo, - .main-container.open-hamburger-menu .header .branding cds-icon { - display: inline-block; - } - .main-container.open-hamburger-menu .header .branding .clr-vmw-logo, - .main-container.open-hamburger-menu - .header - .branding - cds-icon[shape='vm-bug'] { - background-color: #8c8c8c; - border-radius: 0.15rem; - } - .main-container.open-hamburger-menu .header .branding .title { - color: #666; - color: var(--clr-sliding-panel-text-color, #666); - text-overflow: ellipsis; - overflow: hidden; - } - .main-container.open-hamburger-menu .header-hamburger-trigger { - position: fixed; - top: 0; - right: auto; - left: 0; - z-index: 1039; - -webkit-transform: translateX(18.6rem); - transform: translateX(18.6rem); - -webkit-transition: -webkit-transform 0.6s ease; - transition: -webkit-transform 0.6s ease; - transition: transform 0.6s ease; - transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; - } - .main-container.open-hamburger-menu .header-hamburger-trigger::after { - content: none; - } - .main-container.open-hamburger-menu .header-hamburger-trigger > span { - background: 0 0; - } - .main-container.open-hamburger-menu .header-hamburger-trigger > span::after, - .main-container.open-hamburger-menu - .header-hamburger-trigger - > span::before { - left: 0.15rem; - -webkit-transform-origin: 9%; - transform-origin: 9%; - -webkit-transition: -webkit-transform 0.6s ease; - transition: -webkit-transform 0.6s ease; - transition: transform 0.6s ease; - transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; - } - .main-container.open-hamburger-menu - .header-hamburger-trigger - > span::before { - -webkit-transform: rotate(45deg); - transform: rotate(45deg); - } - .main-container.open-hamburger-menu - .header-hamburger-trigger - > span::after { - -webkit-transform: rotate(-45deg); - transform: rotate(-45deg); - } - .main-container.open-hamburger-menu .clr-vertical-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .header-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .sidenav.clr-nav-level-1, - .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .subnav.clr-nav-level-1 { - padding-top: 4.2rem; - -webkit-transform: translateX(0); - transform: translateX(0); - -webkit-transition: -webkit-transform 0.3s ease; - transition: -webkit-transform 0.3s ease; - transition: transform 0.3s ease; - transition: - transform 0.3s ease, - -webkit-transform 0.3s ease; - } - .main-container.open-hamburger-menu - .clr-vertical-nav.clr-nav-level-1 - .sidenav-content, - .main-container.open-hamburger-menu - .header-nav.clr-nav-level-1 - .sidenav-content, - .main-container.open-hamburger-menu - .sidenav.clr-nav-level-1 - .sidenav-content, - .main-container.open-hamburger-menu - .sub-nav.clr-nav-level-1 - .sidenav-content, - .main-container.open-hamburger-menu - .subnav.clr-nav-level-1 - .sidenav-content { - padding-bottom: 1.2rem; - } - .main-container.open-overflow-menu .clr-vertical-nav.clr-nav-level-2, - .main-container.open-overflow-menu .header-nav.clr-nav-level-2, - .main-container.open-overflow-menu .sidenav.clr-nav-level-2, - .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, - .main-container.open-overflow-menu .subnav.clr-nav-level-2 { - -webkit-transform: translateX(0); - transform: translateX(0); - -webkit-transition: -webkit-transform 0.3s ease; - transition: -webkit-transform 0.3s ease; - transition: transform 0.3s ease; - transition: - transform 0.3s ease, - -webkit-transform 0.3s ease; - } - .main-container.open-overflow-menu .header-nav.clr-nav-level-2, - .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, - .main-container.open-overflow-menu .subnav.clr-nav-level-2 { - padding-top: 1.2rem; - } - .main-container.open-overflow-menu .header-overflow-trigger { - position: fixed; - top: 0; - right: 0; - left: auto; - z-index: 1039; - -webkit-transform: translateX(-18.6rem); - transform: translateX(-18.6rem); - -webkit-transition: -webkit-transform 0.6s ease; - transition: -webkit-transform 0.6s ease; - transition: transform 0.6s ease; - transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; - } - .main-container.open-overflow-menu .header-overflow-trigger::after { - content: none; - } - .main-container.open-overflow-menu .header-overflow-trigger > span { - background: 0 0; - } - .main-container.open-overflow-menu .header-overflow-trigger > span::after, - .main-container.open-overflow-menu .header-overflow-trigger > span::before { - height: 0.1rem; - width: 1.2rem; - left: -0.3rem; - -webkit-transform-origin: -3%; - transform-origin: -3%; - -webkit-transition: -webkit-transform 0.6s ease; - transition: -webkit-transform 0.6s ease; - transition: transform 0.6s ease; - transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; - } - .main-container.open-overflow-menu .header-overflow-trigger > span::before { - -webkit-transform: rotate(45deg); - transform: rotate(45deg); - } - .main-container.open-overflow-menu .header-overflow-trigger > span::after { - -webkit-transform: rotate(-45deg); - transform: rotate(-45deg); - } - .main-container.open-hamburger-menu .clr-vertical-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .header-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .sidenav.clr-nav-level-1, - .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .subnav.clr-nav-level-1 { - width: 18rem; - max-width: 18rem; - } - .main-container.open-overflow-menu .clr-vertical-nav.clr-nav-level-2, - .main-container.open-overflow-menu .header-nav.clr-nav-level-2, - .main-container.open-overflow-menu .sidenav.clr-nav-level-2, - .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, - .main-container.open-overflow-menu .subnav.clr-nav-level-2 { - width: 18rem; - max-width: 18rem; - } + } + .main-container.open-hamburger-menu .clr-vertical-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .header-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .sidenav.clr-nav-level-1, + .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .subnav.clr-nav-level-1 { + width: 18rem; + max-width: 18rem; + } + .main-container.open-overflow-menu .clr-vertical-nav.clr-nav-level-2, + .main-container.open-overflow-menu .header-nav.clr-nav-level-2, + .main-container.open-overflow-menu .sidenav.clr-nav-level-2, + .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, + .main-container.open-overflow-menu .subnav.clr-nav-level-2 { + width: 18rem; + max-width: 18rem; + } } @media screen and (max-width: 576px) { - .main-container .header .branding { - max-width: 7.2rem; - min-width: 0; - overflow: hidden; - } - .main-container .clr-vertical-nav.clr-nav-level-1, - .main-container .header-nav.clr-nav-level-1, - .main-container .sidenav.clr-nav-level-1, - .main-container .sub-nav.clr-nav-level-1, - .main-container .subnav.clr-nav-level-1 { - -webkit-transform: translateX(-14.4rem); - transform: translateX(-14.4rem); - } - .main-container .clr-vertical-nav.clr-nav-level-2, - .main-container .header-nav.clr-nav-level-2, - .main-container .sidenav.clr-nav-level-2, - .main-container .sub-nav.clr-nav-level-2, - .main-container .subnav.clr-nav-level-2 { - -webkit-transform: translateX(14.4rem); - transform: translateX(14.4rem); - } - .main-container.open-hamburger-menu .header .branding { - width: 14.4rem; - max-width: 14.4rem; - } - .main-container.open-hamburger-menu .clr-vertical-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .header-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .sidenav.clr-nav-level-1, - .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1, - .main-container.open-hamburger-menu .subnav.clr-nav-level-1 { - width: 14.4rem; - max-width: 14.4rem; - } - .main-container.open-hamburger-menu .header-hamburger-trigger { - position: fixed; - top: 0; - right: auto; - left: 0; - z-index: 1039; - -webkit-transform: translateX(15rem); - transform: translateX(15rem); - -webkit-transition: -webkit-transform 0.6s ease; - transition: -webkit-transform 0.6s ease; - transition: transform 0.6s ease; - transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; - } - .main-container.open-hamburger-menu .header-hamburger-trigger::after { - content: none; - } - .main-container.open-overflow-menu .clr-vertical-nav.clr-nav-level-2, - .main-container.open-overflow-menu .header-nav.clr-nav-level-2, - .main-container.open-overflow-menu .sidenav.clr-nav-level-2, - .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, - .main-container.open-overflow-menu .subnav.clr-nav-level-2 { - width: 14.4rem; - max-width: 14.4rem; - } - .main-container.open-overflow-menu .header-overflow-trigger { - position: fixed; - top: 0; - right: 0; - left: auto; - z-index: 1039; - -webkit-transform: translateX(-15rem); - transform: translateX(-15rem); - -webkit-transition: -webkit-transform 0.6s ease; - transition: -webkit-transform 0.6s ease; - transition: transform 0.6s ease; - transition: - transform 0.6s ease, - -webkit-transform 0.6s ease; - } - .main-container.open-overflow-menu .header-overflow-trigger::after { - content: none; - } + .main-container .header .branding { + max-width: 7.2rem; + min-width: 0; + overflow: hidden; + } + .main-container .clr-vertical-nav.clr-nav-level-1, + .main-container .header-nav.clr-nav-level-1, + .main-container .sidenav.clr-nav-level-1, + .main-container .sub-nav.clr-nav-level-1, + .main-container .subnav.clr-nav-level-1 { + -webkit-transform: translateX(-14.4rem); + transform: translateX(-14.4rem); + } + .main-container .clr-vertical-nav.clr-nav-level-2, + .main-container .header-nav.clr-nav-level-2, + .main-container .sidenav.clr-nav-level-2, + .main-container .sub-nav.clr-nav-level-2, + .main-container .subnav.clr-nav-level-2 { + -webkit-transform: translateX(14.4rem); + transform: translateX(14.4rem); + } + .main-container.open-hamburger-menu .header .branding { + width: 14.4rem; + max-width: 14.4rem; + } + .main-container.open-hamburger-menu .clr-vertical-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .header-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .sidenav.clr-nav-level-1, + .main-container.open-hamburger-menu .sub-nav.clr-nav-level-1, + .main-container.open-hamburger-menu .subnav.clr-nav-level-1 { + width: 14.4rem; + max-width: 14.4rem; + } + .main-container.open-hamburger-menu .header-hamburger-trigger { + position: fixed; + top: 0; + right: auto; + left: 0; + z-index: 1039; + -webkit-transform: translateX(15rem); + transform: translateX(15rem); + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: + transform 0.6s ease, + -webkit-transform 0.6s ease; + } + .main-container.open-hamburger-menu .header-hamburger-trigger::after { + content: none; + } + .main-container.open-overflow-menu .clr-vertical-nav.clr-nav-level-2, + .main-container.open-overflow-menu .header-nav.clr-nav-level-2, + .main-container.open-overflow-menu .sidenav.clr-nav-level-2, + .main-container.open-overflow-menu .sub-nav.clr-nav-level-2, + .main-container.open-overflow-menu .subnav.clr-nav-level-2 { + width: 14.4rem; + max-width: 14.4rem; + } + .main-container.open-overflow-menu .header-overflow-trigger { + position: fixed; + top: 0; + right: 0; + left: auto; + z-index: 1039; + -webkit-transform: translateX(-15rem); + transform: translateX(-15rem); + -webkit-transition: -webkit-transform 0.6s ease; + transition: -webkit-transform 0.6s ease; + transition: transform 0.6s ease; + transition: + transform 0.6s ease, + -webkit-transform 0.6s ease; + } + .main-container.open-overflow-menu .header-overflow-trigger::after { + content: none; + } } :root { - --clr-progress-default-color: var(--clr-color-action-600); - --clr-progress-alt-color-1: var(--clr-color-success-400); - --clr-progress-alt-color-2: var(--clr-color-danger-800); - --clr-progress-alt-color-3: var(--clr-progress-alt-color-2); - --clr-progress-bg-color: var(--clr-color-neutral-200); + --clr-progress-default-color: var(--clr-color-action-600); + --clr-progress-alt-color-1: var(--clr-color-success-400); + --clr-progress-alt-color-2: var(--clr-color-danger-800); + --clr-progress-alt-color-3: var(--clr-progress-alt-color-2); + --clr-progress-bg-color: var(--clr-color-neutral-200); } .progress, .progress-static { - background-color: transparent; - border-radius: 0; - font-size: inherit; - height: 2em; - margin: 0; - max-height: 0.7rem; - min-height: 0.2rem; - overflow: hidden; - display: block; - width: 100%; + background-color: transparent; + border-radius: 0; + font-size: inherit; + height: 2em; + margin: 0; + max-height: 0.7rem; + min-height: 0.2rem; + overflow: hidden; + display: block; + width: 100%; } .progress > progress { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - color: #0072a3; - color: var(--clr-progress-default-color, #0072a3); - display: block; - background-color: #e8e8e8; - background-color: var(--clr-progress-bg-color, #e8e8e8); - border: none; - height: 100%; - width: 100%; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + color: #0072a3; + color: var(--clr-progress-default-color, #0072a3); + display: block; + background-color: #e8e8e8; + background-color: var(--clr-progress-bg-color, #e8e8e8); + border: none; + height: 100%; + width: 100%; } .progress > progress::-webkit-progress-value { - background-color: #0072a3; - background-color: var(--clr-progress-default-color, #0072a3); + background-color: #0072a3; + background-color: var(--clr-progress-default-color, #0072a3); } .progress > progress::-moz-progress-bar { - background-color: #0072a3; - background-color: var(--clr-progress-default-color, #0072a3); -} -.progress > progress[value='0']::-moz-progress-bar { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - color: #e8e8e8; - color: var(--clr-progress-bg-color, #e8e8e8); - min-width: 2.4rem; - background-color: transparent; - background-image: none; -} -.progress > progress[value='0']::-webkit-progress-value { - -webkit-transition: none; - transition: none; + background-color: #0072a3; + background-color: var(--clr-progress-default-color, #0072a3); +} +.progress > progress[value="0"]::-moz-progress-bar { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + color: #e8e8e8; + color: var(--clr-progress-bg-color, #e8e8e8); + min-width: 2.4rem; + background-color: transparent; + background-image: none; +} +.progress > progress[value="0"]::-webkit-progress-value { + -webkit-transition: none; + transition: none; } .progress > progress::-webkit-progress-bar { - border-radius: 0; - background-color: #e8e8e8; - background-color: var(--clr-progress-bg-color, #e8e8e8); + border-radius: 0; + background-color: #e8e8e8; + background-color: var(--clr-progress-bg-color, #e8e8e8); } .progress > progress::-webkit-progress-inner-element { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; } .progress > progress::-webkit-progress-value { - -webkit-transition: width 0.23s ease-in; - transition: width 0.23s ease-in; - border-radius: 0; + -webkit-transition: width 0.23s ease-in; + transition: width 0.23s ease-in; + border-radius: 0; } .progress.success > progress { - color: #5eb715; - color: var(--clr-progress-alt-color-1, #5eb715); + color: #5eb715; + color: var(--clr-progress-alt-color-1, #5eb715); } .progress.success > progress::-webkit-progress-value { - background-color: #5eb715; - background-color: var(--clr-progress-alt-color-1, #5eb715); + background-color: #5eb715; + background-color: var(--clr-progress-alt-color-1, #5eb715); } .progress.success > progress::-moz-progress-bar { - background-color: #5eb715; - background-color: var(--clr-progress-alt-color-1, #5eb715); + background-color: #5eb715; + background-color: var(--clr-progress-alt-color-1, #5eb715); } .progress.warning > progress { - color: #c21d00; - color: var(--clr-progress-alt-color-3, #c21d00); + color: #c21d00; + color: var(--clr-progress-alt-color-3, #c21d00); } .progress.warning > progress::-webkit-progress-value { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-3, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-3, #c21d00); } .progress.warning > progress::-moz-progress-bar { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-3, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-3, #c21d00); } .progress.danger > progress { - color: #c21d00; - color: var(--clr-progress-alt-color-2, #c21d00); + color: #c21d00; + color: var(--clr-progress-alt-color-2, #c21d00); } .progress.danger > progress::-webkit-progress-value { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-2, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-2, #c21d00); } .progress.danger > progress::-moz-progress-bar { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-2, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-2, #c21d00); } .progress-static.labeled, .progress.labeled { - position: relative; - padding-right: 3em; + position: relative; + padding-right: 3em; } .progress-static.labeled > span, .progress.labeled > span { - display: block; - font-size: 1em; - position: absolute; - top: 50%; - right: 0; - line-height: 1em; - margin-top: -0.375em; + display: block; + font-size: 1em; + position: absolute; + top: 50%; + right: 0; + line-height: 1em; + margin-top: -0.375em; } @-webkit-keyframes clr-progress-fade { - from { - opacity: 1; - } - to { - opacity: 0; - } + from { + opacity: 1; + } + to { + opacity: 0; + } } @keyframes clr-progress-fade { - from { - opacity: 1; - } - to { - opacity: 0; - } -} -.progress.progress-fade > progress[value='100'], -.progress.progress-fade > progress[value='100'] + span { - -webkit-animation: clr-progress-fade 0.3s linear 0.5s forwards; - animation: clr-progress-fade 0.3s linear 0.5s forwards; + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.progress.progress-fade > progress[value="100"], +.progress.progress-fade > progress[value="100"] + span { + -webkit-animation: clr-progress-fade 0.3s linear 0.5s forwards; + animation: clr-progress-fade 0.3s linear 0.5s forwards; } .progress.flash-danger > progress, .progress.flash > progress { - -webkit-transition: color 0.1s ease-out 1s; - transition: color 0.1s ease-out 1s; + -webkit-transition: color 0.1s ease-out 1s; + transition: color 0.1s ease-out 1s; } .progress.flash-danger > progress::-webkit-progress-value, .progress.flash > progress::-webkit-progress-value { - -webkit-transition: - width 0.23s ease-in, - background-color 0.1s ease-out 0.3s; - transition: - width 0.23s ease-in, - background-color 0.1s ease-out 0.3s; -} -.progress.flash-danger > progress[value='0']::-webkit-progress-value, -.progress.flash > progress[value='0']::-webkit-progress-value { - -webkit-transition: none; - transition: none; + -webkit-transition: + width 0.23s ease-in, + background-color 0.1s ease-out 0.3s; + transition: + width 0.23s ease-in, + background-color 0.1s ease-out 0.3s; +} +.progress.flash-danger > progress[value="0"]::-webkit-progress-value, +.progress.flash > progress[value="0"]::-webkit-progress-value { + -webkit-transition: none; + transition: none; } .progress.flash-danger > progress::-moz-progress-bar, .progress.flash > progress::-moz-progress-bar { - -moz-transition: - width 0.23s ease-in, - background-color 0.1s ease-out 0.3s; - transition: - width 0.23s ease-in, - background-color 0.1s ease-out 0.3s; -} -.progress.flash > progress[value='100'] { - color: #5eb715; - color: var(--clr-progress-alt-color-1, #5eb715); -} -.progress.flash > progress[value='100']::-webkit-progress-value { - background-color: #5eb715; - background-color: var(--clr-progress-alt-color-1, #5eb715); -} -.progress.flash > progress[value='100']::-moz-progress-bar { - background-color: #5eb715; - background-color: var(--clr-progress-alt-color-1, #5eb715); -} -.progress.progress-fade.flash > progress[value='100'], -.progress.progress-fade.flash > progress[value='100'] + span { - -webkit-animation: clr-progress-fade 0.6s linear 1s forwards; - animation: clr-progress-fade 0.6s linear 1s forwards; -} -.progress.flash-danger > progress[value='100'] { - color: #c21d00; - color: var(--clr-progress-alt-color-2, #c21d00); -} -.progress.flash-danger > progress[value='100']::-webkit-progress-value { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-2, #c21d00); -} -.progress.flash-danger > progress[value='100']::-moz-progress-bar { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-2, #c21d00); + -moz-transition: + width 0.23s ease-in, + background-color 0.1s ease-out 0.3s; + transition: + width 0.23s ease-in, + background-color 0.1s ease-out 0.3s; +} +.progress.flash > progress[value="100"] { + color: #5eb715; + color: var(--clr-progress-alt-color-1, #5eb715); +} +.progress.flash > progress[value="100"]::-webkit-progress-value { + background-color: #5eb715; + background-color: var(--clr-progress-alt-color-1, #5eb715); +} +.progress.flash > progress[value="100"]::-moz-progress-bar { + background-color: #5eb715; + background-color: var(--clr-progress-alt-color-1, #5eb715); +} +.progress.progress-fade.flash > progress[value="100"], +.progress.progress-fade.flash > progress[value="100"] + span { + -webkit-animation: clr-progress-fade 0.6s linear 1s forwards; + animation: clr-progress-fade 0.6s linear 1s forwards; +} +.progress.flash-danger > progress[value="100"] { + color: #c21d00; + color: var(--clr-progress-alt-color-2, #c21d00); +} +.progress.flash-danger > progress[value="100"]::-webkit-progress-value { + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-2, #c21d00); +} +.progress.flash-danger > progress[value="100"]::-moz-progress-bar { + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-2, #c21d00); } @-webkit-keyframes clr-progress-looper { - from { - left: -100%; - } - to { - left: 100%; - } + from { + left: -100%; + } + to { + left: 100%; + } } @keyframes clr-progress-looper { - from { - left: -100%; - } - to { - left: 100%; - } + from { + left: -100%; + } + to { + left: 100%; + } } .progress.loop { - position: relative; + position: relative; } .progress.loop > progress { - overflow: hidden; - color: transparent; - color: var(--clr-color-action-600, transparent); + overflow: hidden; + color: transparent; + color: var(--clr-color-action-600, transparent); } .progress.loop > progress::-webkit-progress-value { - background-color: transparent; - background-color: var(--clr-color-action-600, transparent); + background-color: transparent; + background-color: var(--clr-color-action-600, transparent); } .progress.loop > progress::-moz-progress-bar { - background-color: transparent; - background-color: var(--clr-color-action-600, transparent); + background-color: transparent; + background-color: var(--clr-color-action-600, transparent); } .progress.loop > progress::-moz-progress-bar { - background-color: transparent; + background-color: transparent; } .progress.loop::after { - -webkit-animation: clr-progress-looper 2s ease-in-out infinite; - animation: clr-progress-looper 2s ease-in-out infinite; - content: ' '; - top: 0; - bottom: 0; - left: 0; - position: absolute; - display: block; - background-color: #0072a3; - background-color: var(--clr-progress-default-color, #0072a3); - width: 75%; + -webkit-animation: clr-progress-looper 2s ease-in-out infinite; + animation: clr-progress-looper 2s ease-in-out infinite; + content: " "; + top: 0; + bottom: 0; + left: 0; + position: absolute; + display: block; + background-color: #0072a3; + background-color: var(--clr-progress-default-color, #0072a3); + width: 75%; } .progress.loop.danger::after, .progress.loop.warning::after { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-2, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-2, #c21d00); } .progress.loop.success::after { - background-color: #5eb715; - background-color: var(--clr-progress-alt-color-1, #5eb715); + background-color: #5eb715; + background-color: var(--clr-progress-alt-color-1, #5eb715); } .nav-item .progress::after { - top: 0; + top: 0; } .progress-static { - position: relative; - border: none; - width: 100%; + position: relative; + border: none; + width: 100%; } .progress-static > .progress-meter { - background-color: #e8e8e8; - background-color: var(--clr-progress-bg-color, #e8e8e8); - display: block; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; + background-color: #e8e8e8; + background-color: var(--clr-progress-bg-color, #e8e8e8); + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; } .progress-static > .progress-meter::before { - background-color: #0072a3; - background-color: var(--clr-progress-default-color, #0072a3); - top: 0; - bottom: 0; - left: 0; - position: absolute; - display: block; - width: 0%; - content: ' '; -} -.progress-static > .progress-meter[data-value='1']::before, -.progress-static > .progress-meter[data-value='2']::before, -.progress-static > .progress-meter[data-value='3']::before { - width: 2%; -} -.progress-static > .progress-meter[data-value='4']::before, -.progress-static > .progress-meter[data-value='5']::before, -.progress-static > .progress-meter[data-value='6']::before, -.progress-static > .progress-meter[data-value='7']::before { - width: 5%; -} -.progress-static > .progress-meter[data-value='10']::before, -.progress-static > .progress-meter[data-value='11']::before, -.progress-static > .progress-meter[data-value='12']::before, -.progress-static > .progress-meter[data-value='8']::before, -.progress-static > .progress-meter[data-value='9']::before { - width: 10%; -} -.progress-static > .progress-meter[data-value='13']::before, -.progress-static > .progress-meter[data-value='14']::before, -.progress-static > .progress-meter[data-value='15']::before, -.progress-static > .progress-meter[data-value='16']::before, -.progress-static > .progress-meter[data-value='17']::before { - width: 15%; -} -.progress-static > .progress-meter[data-value='18']::before, -.progress-static > .progress-meter[data-value='19']::before, -.progress-static > .progress-meter[data-value='20']::before, -.progress-static > .progress-meter[data-value='21']::before, -.progress-static > .progress-meter[data-value='22']::before { - width: 20%; -} -.progress-static > .progress-meter[data-value='23']::before, -.progress-static > .progress-meter[data-value='24']::before, -.progress-static > .progress-meter[data-value='25']::before, -.progress-static > .progress-meter[data-value='26']::before, -.progress-static > .progress-meter[data-value='27']::before { - width: 25%; -} -.progress-static > .progress-meter[data-value='28']::before, -.progress-static > .progress-meter[data-value='29']::before, -.progress-static > .progress-meter[data-value='30']::before, -.progress-static > .progress-meter[data-value='31']::before, -.progress-static > .progress-meter[data-value='32']::before { - width: 30%; -} -.progress-static > .progress-meter[data-value='33']::before, -.progress-static > .progress-meter[data-value='34']::before, -.progress-static > .progress-meter[data-value='35']::before, -.progress-static > .progress-meter[data-value='36']::before, -.progress-static > .progress-meter[data-value='37']::before { - width: 35%; -} -.progress-static > .progress-meter[data-value='38']::before, -.progress-static > .progress-meter[data-value='39']::before, -.progress-static > .progress-meter[data-value='40']::before, -.progress-static > .progress-meter[data-value='41']::before, -.progress-static > .progress-meter[data-value='42']::before { - width: 40%; -} -.progress-static > .progress-meter[data-value='43']::before, -.progress-static > .progress-meter[data-value='44']::before, -.progress-static > .progress-meter[data-value='45']::before, -.progress-static > .progress-meter[data-value='46']::before, -.progress-static > .progress-meter[data-value='47']::before { - width: 45%; -} -.progress-static > .progress-meter[data-value='48']::before, -.progress-static > .progress-meter[data-value='49']::before, -.progress-static > .progress-meter[data-value='50']::before, -.progress-static > .progress-meter[data-value='51']::before, -.progress-static > .progress-meter[data-value='52']::before { - width: 50%; -} -.progress-static > .progress-meter[data-value='53']::before, -.progress-static > .progress-meter[data-value='54']::before, -.progress-static > .progress-meter[data-value='55']::before, -.progress-static > .progress-meter[data-value='56']::before, -.progress-static > .progress-meter[data-value='57']::before { - width: 55%; -} -.progress-static > .progress-meter[data-value='58']::before, -.progress-static > .progress-meter[data-value='59']::before, -.progress-static > .progress-meter[data-value='60']::before, -.progress-static > .progress-meter[data-value='61']::before, -.progress-static > .progress-meter[data-value='62']::before { - width: 60%; -} -.progress-static > .progress-meter[data-value='63']::before, -.progress-static > .progress-meter[data-value='64']::before, -.progress-static > .progress-meter[data-value='65']::before, -.progress-static > .progress-meter[data-value='66']::before, -.progress-static > .progress-meter[data-value='67']::before { - width: 65%; -} -.progress-static > .progress-meter[data-value='68']::before, -.progress-static > .progress-meter[data-value='69']::before, -.progress-static > .progress-meter[data-value='70']::before, -.progress-static > .progress-meter[data-value='71']::before, -.progress-static > .progress-meter[data-value='72']::before { - width: 70%; -} -.progress-static > .progress-meter[data-value='73']::before, -.progress-static > .progress-meter[data-value='74']::before, -.progress-static > .progress-meter[data-value='75']::before, -.progress-static > .progress-meter[data-value='76']::before, -.progress-static > .progress-meter[data-value='77']::before { - width: 75%; -} -.progress-static > .progress-meter[data-value='78']::before, -.progress-static > .progress-meter[data-value='79']::before, -.progress-static > .progress-meter[data-value='80']::before, -.progress-static > .progress-meter[data-value='81']::before, -.progress-static > .progress-meter[data-value='82']::before { - width: 80%; -} -.progress-static > .progress-meter[data-value='83']::before, -.progress-static > .progress-meter[data-value='84']::before, -.progress-static > .progress-meter[data-value='85']::before, -.progress-static > .progress-meter[data-value='86']::before, -.progress-static > .progress-meter[data-value='87']::before { - width: 85%; -} -.progress-static > .progress-meter[data-value='88']::before, -.progress-static > .progress-meter[data-value='89']::before, -.progress-static > .progress-meter[data-value='90']::before, -.progress-static > .progress-meter[data-value='91']::before, -.progress-static > .progress-meter[data-value='92']::before { - width: 90%; -} -.progress-static > .progress-meter[data-value='93']::before, -.progress-static > .progress-meter[data-value='94']::before, -.progress-static > .progress-meter[data-value='95']::before, -.progress-static > .progress-meter[data-value='96']::before { - width: 95%; -} -.progress-static > .progress-meter[data-value='97']::before, -.progress-static > .progress-meter[data-value='98']::before, -.progress-static > .progress-meter[data-value='99']::before { - width: 98%; -} -.progress-static > .progress-meter[data-value='100']::before { - width: 100%; + background-color: #0072a3; + background-color: var(--clr-progress-default-color, #0072a3); + top: 0; + bottom: 0; + left: 0; + position: absolute; + display: block; + width: 0%; + content: " "; +} +.progress-static > .progress-meter[data-value="1"]::before, +.progress-static > .progress-meter[data-value="2"]::before, +.progress-static > .progress-meter[data-value="3"]::before { + width: 2%; +} +.progress-static > .progress-meter[data-value="4"]::before, +.progress-static > .progress-meter[data-value="5"]::before, +.progress-static > .progress-meter[data-value="6"]::before, +.progress-static > .progress-meter[data-value="7"]::before { + width: 5%; +} +.progress-static > .progress-meter[data-value="10"]::before, +.progress-static > .progress-meter[data-value="11"]::before, +.progress-static > .progress-meter[data-value="12"]::before, +.progress-static > .progress-meter[data-value="8"]::before, +.progress-static > .progress-meter[data-value="9"]::before { + width: 10%; +} +.progress-static > .progress-meter[data-value="13"]::before, +.progress-static > .progress-meter[data-value="14"]::before, +.progress-static > .progress-meter[data-value="15"]::before, +.progress-static > .progress-meter[data-value="16"]::before, +.progress-static > .progress-meter[data-value="17"]::before { + width: 15%; +} +.progress-static > .progress-meter[data-value="18"]::before, +.progress-static > .progress-meter[data-value="19"]::before, +.progress-static > .progress-meter[data-value="20"]::before, +.progress-static > .progress-meter[data-value="21"]::before, +.progress-static > .progress-meter[data-value="22"]::before { + width: 20%; +} +.progress-static > .progress-meter[data-value="23"]::before, +.progress-static > .progress-meter[data-value="24"]::before, +.progress-static > .progress-meter[data-value="25"]::before, +.progress-static > .progress-meter[data-value="26"]::before, +.progress-static > .progress-meter[data-value="27"]::before { + width: 25%; +} +.progress-static > .progress-meter[data-value="28"]::before, +.progress-static > .progress-meter[data-value="29"]::before, +.progress-static > .progress-meter[data-value="30"]::before, +.progress-static > .progress-meter[data-value="31"]::before, +.progress-static > .progress-meter[data-value="32"]::before { + width: 30%; +} +.progress-static > .progress-meter[data-value="33"]::before, +.progress-static > .progress-meter[data-value="34"]::before, +.progress-static > .progress-meter[data-value="35"]::before, +.progress-static > .progress-meter[data-value="36"]::before, +.progress-static > .progress-meter[data-value="37"]::before { + width: 35%; +} +.progress-static > .progress-meter[data-value="38"]::before, +.progress-static > .progress-meter[data-value="39"]::before, +.progress-static > .progress-meter[data-value="40"]::before, +.progress-static > .progress-meter[data-value="41"]::before, +.progress-static > .progress-meter[data-value="42"]::before { + width: 40%; +} +.progress-static > .progress-meter[data-value="43"]::before, +.progress-static > .progress-meter[data-value="44"]::before, +.progress-static > .progress-meter[data-value="45"]::before, +.progress-static > .progress-meter[data-value="46"]::before, +.progress-static > .progress-meter[data-value="47"]::before { + width: 45%; +} +.progress-static > .progress-meter[data-value="48"]::before, +.progress-static > .progress-meter[data-value="49"]::before, +.progress-static > .progress-meter[data-value="50"]::before, +.progress-static > .progress-meter[data-value="51"]::before, +.progress-static > .progress-meter[data-value="52"]::before { + width: 50%; +} +.progress-static > .progress-meter[data-value="53"]::before, +.progress-static > .progress-meter[data-value="54"]::before, +.progress-static > .progress-meter[data-value="55"]::before, +.progress-static > .progress-meter[data-value="56"]::before, +.progress-static > .progress-meter[data-value="57"]::before { + width: 55%; +} +.progress-static > .progress-meter[data-value="58"]::before, +.progress-static > .progress-meter[data-value="59"]::before, +.progress-static > .progress-meter[data-value="60"]::before, +.progress-static > .progress-meter[data-value="61"]::before, +.progress-static > .progress-meter[data-value="62"]::before { + width: 60%; +} +.progress-static > .progress-meter[data-value="63"]::before, +.progress-static > .progress-meter[data-value="64"]::before, +.progress-static > .progress-meter[data-value="65"]::before, +.progress-static > .progress-meter[data-value="66"]::before, +.progress-static > .progress-meter[data-value="67"]::before { + width: 65%; +} +.progress-static > .progress-meter[data-value="68"]::before, +.progress-static > .progress-meter[data-value="69"]::before, +.progress-static > .progress-meter[data-value="70"]::before, +.progress-static > .progress-meter[data-value="71"]::before, +.progress-static > .progress-meter[data-value="72"]::before { + width: 70%; +} +.progress-static > .progress-meter[data-value="73"]::before, +.progress-static > .progress-meter[data-value="74"]::before, +.progress-static > .progress-meter[data-value="75"]::before, +.progress-static > .progress-meter[data-value="76"]::before, +.progress-static > .progress-meter[data-value="77"]::before { + width: 75%; +} +.progress-static > .progress-meter[data-value="78"]::before, +.progress-static > .progress-meter[data-value="79"]::before, +.progress-static > .progress-meter[data-value="80"]::before, +.progress-static > .progress-meter[data-value="81"]::before, +.progress-static > .progress-meter[data-value="82"]::before { + width: 80%; +} +.progress-static > .progress-meter[data-value="83"]::before, +.progress-static > .progress-meter[data-value="84"]::before, +.progress-static > .progress-meter[data-value="85"]::before, +.progress-static > .progress-meter[data-value="86"]::before, +.progress-static > .progress-meter[data-value="87"]::before { + width: 85%; +} +.progress-static > .progress-meter[data-value="88"]::before, +.progress-static > .progress-meter[data-value="89"]::before, +.progress-static > .progress-meter[data-value="90"]::before, +.progress-static > .progress-meter[data-value="91"]::before, +.progress-static > .progress-meter[data-value="92"]::before { + width: 90%; +} +.progress-static > .progress-meter[data-value="93"]::before, +.progress-static > .progress-meter[data-value="94"]::before, +.progress-static > .progress-meter[data-value="95"]::before, +.progress-static > .progress-meter[data-value="96"]::before { + width: 95%; +} +.progress-static > .progress-meter[data-value="97"]::before, +.progress-static > .progress-meter[data-value="98"]::before, +.progress-static > .progress-meter[data-value="99"]::before { + width: 98%; +} +.progress-static > .progress-meter[data-value="100"]::before { + width: 100%; } .progress-static.labeled > .progress-meter { - right: 3em; + right: 3em; } .progress-static.success > .progress-meter::before { - background-color: #5eb715; - background-color: var(--clr-progress-alt-color-1, #5eb715); + background-color: #5eb715; + background-color: var(--clr-progress-alt-color-1, #5eb715); } .progress-static.warning > .progress-meter::before { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-3, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-3, #c21d00); } .progress-static.danger > .progress-meter::before { - background-color: #c21d00; - background-color: var(--clr-progress-alt-color-2, #c21d00); + background-color: #c21d00; + background-color: var(--clr-progress-alt-color-2, #c21d00); } .card-block .progress, .card-block .progress-static, .card-footer .progress, .card-footer .progress-static { - margin: 0; - margin-top: -0.6rem; - height: 0.1875rem; - position: absolute; - left: 0; + margin: 0; + margin-top: -0.6rem; + height: 0.1875rem; + position: absolute; + left: 0; } .card-block .progress-static > .progress-meter, .card-block .progress > progress, .card-footer .progress-static > .progress-meter, .card-footer .progress > progress { - height: 0.1875rem; - position: absolute; + height: 0.1875rem; + position: absolute; } .card-block .progress-static.top, .card-block .progress.top, .card-footer .progress-static.top, .card-footer .progress.top { - margin-top: 0; - top: 0; + margin-top: 0; + top: 0; } .nav-item .progress, .nav-item .progress-static { - margin: 0; - height: 0.24rem; - min-height: 0.24rem; - max-height: 0.24rem; - left: 0; + margin: 0; + height: 0.24rem; + min-height: 0.24rem; + max-height: 0.24rem; + left: 0; } .nav-item .progress-static > .progress-meter, .nav-item .progress > progress { - height: 0.24rem; - min-height: 0.24rem; - max-height: 0.24rem; - position: absolute; + height: 0.24rem; + min-height: 0.24rem; + max-height: 0.24rem; + position: absolute; } .progress-block { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - width: 100%; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + width: 100%; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; } .progress-block > * { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - padding-right: 0.6rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + padding-right: 0.6rem; } .progress-block > :first-child { - padding-right: 0.9rem; + padding-right: 0.9rem; } .progress-block > :last-child { - padding-right: 0; + padding-right: 0; } .progress-block > label { - font-weight: 600; + font-weight: 600; } .progress-block > .progress, .progress-block > .progress-static { - -webkit-box-flex: 0; - -ms-flex: 0 1 auto; - flex: 0 1 auto; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; } .progress-block > .progress-group { - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - height: auto; - -webkit-box-flex: 0; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - width: 100%; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + height: auto; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + width: 100%; } .progress-block > .progress-group .clr-row { - margin-left: 0; - margin-right: 0; + margin-left: 0; + margin-right: 0; } -.progress-block > .progress-group .clr-row > [class*='clr-col-'] { - padding-left: 0; - padding-right: 0; +.progress-block > .progress-group .clr-row > [class*="clr-col-"] { + padding-left: 0; + padding-right: 0; } .card-block .progress-block { - margin-bottom: 0.6rem; - padding: 0; + margin-bottom: 0.6rem; + padding: 0; } .card-block .progress-block:last-child { - margin-bottom: 0; + margin-bottom: 0; } .card-block .progress-block > label { - max-width: 33%; - line-height: 0.9rem; + max-width: 33%; + line-height: 0.9rem; } .card-block .progress-block .progress, .card-block .progress-block .progress-static { - position: relative; - height: 0.6395rem; - margin-top: 0; + position: relative; + height: 0.6395rem; + margin-top: 0; } .card-block .progress-block .progress-static > .progress-meter, .card-block .progress-block .progress-static > progress, .card-block .progress-block .progress > .progress-meter, .card-block .progress-block .progress > progress { - height: 0.6395rem; + height: 0.6395rem; } _:-ms-input-placeholder .progress-block > label, :root .progress-block > label { - display: inline-block; + display: inline-block; } .spinner { - position: relative; - display: inline-block; - height: 3.6rem; - width: 3.6rem; - min-height: 3.6rem; - min-width: 3.6rem; - -webkit-animation: spin 1s linear infinite; - animation: spin 1s linear infinite; - margin: 0; - padding: 0; - background: url('data:image/svg+xml;charset=utf8,%3Csvg%20id%3D%22Layer_2%22%20data-name%3D%22Layer%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-miterlimit%3A%2010%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-width%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23000000%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-opacity%3A%200.15%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%230072a3%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3EPreloader_72x2%3C%2Ftitle%3E%0A%20%20%20%20%3Ccircle%20class%3D%22cls-1%22%20cx%3D%2236%22%20cy%3D%2236%22%20r%3D%2233%22%2F%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-2%22%20d%3D%22M14.3%2C60.9A33%2C33%2C0%2C0%2C1%2C36%2C3%22%3E%0A%20%20%20%20%3C%2Fpath%3E%0A%3C%2Fsvg%3E%0A'); - text-indent: 100%; - overflow: hidden; - white-space: nowrap; + position: relative; + display: inline-block; + height: 3.6rem; + width: 3.6rem; + min-height: 3.6rem; + min-width: 3.6rem; + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; + margin: 0; + padding: 0; + background: url("data:image/svg+xml;charset=utf8,%3Csvg%20id%3D%22Layer_2%22%20data-name%3D%22Layer%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-miterlimit%3A%2010%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-width%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23000000%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-opacity%3A%200.15%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%230072a3%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3EPreloader_72x2%3C%2Ftitle%3E%0A%20%20%20%20%3Ccircle%20class%3D%22cls-1%22%20cx%3D%2236%22%20cy%3D%2236%22%20r%3D%2233%22%2F%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-2%22%20d%3D%22M14.3%2C60.9A33%2C33%2C0%2C0%2C1%2C36%2C3%22%3E%0A%20%20%20%20%3C%2Fpath%3E%0A%3C%2Fsvg%3E%0A"); + text-indent: 100%; + overflow: hidden; + white-space: nowrap; } .spinner.spinner-md { - height: 1.8rem; - width: 1.8rem; - min-height: 1.8rem; - min-width: 1.8rem; + height: 1.8rem; + width: 1.8rem; + min-height: 1.8rem; + min-width: 1.8rem; } .spinner.spinner-inline, .spinner.spinner-sm { - height: 0.9rem; - width: 0.9rem; - min-height: 0.9rem; - min-width: 0.9rem; + height: 0.9rem; + width: 0.9rem; + min-height: 0.9rem; + min-width: 0.9rem; } .spinner.spinner-inline { - vertical-align: text-bottom; + vertical-align: text-bottom; } .spinner.spinner-inverse { - background: url('data:image/svg+xml;charset=utf8,%3Csvg%20id%3D%22Layer_2%22%20data-name%3D%22Layer%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-miterlimit%3A%2010%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-width%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23ffffff%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-opacity%3A%200.15%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%2374c1e2%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3EPreloader_72x2%3C%2Ftitle%3E%0A%20%20%20%20%3Ccircle%20class%3D%22cls-1%22%20cx%3D%2236%22%20cy%3D%2236%22%20r%3D%2233%22%2F%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-2%22%20d%3D%22M14.3%2C60.9A33%2C33%2C0%2C0%2C1%2C36%2C3%22%3E%0A%20%20%20%20%3C%2Fpath%3E%0A%3C%2Fsvg%3E%0A'); + background: url("data:image/svg+xml;charset=utf8,%3Csvg%20id%3D%22Layer_2%22%20data-name%3D%22Layer%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-miterlimit%3A%2010%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-width%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23ffffff%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-opacity%3A%200.15%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%2374c1e2%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3EPreloader_72x2%3C%2Ftitle%3E%0A%20%20%20%20%3Ccircle%20class%3D%22cls-1%22%20cx%3D%2236%22%20cy%3D%2236%22%20r%3D%2233%22%2F%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-2%22%20d%3D%22M14.3%2C60.9A33%2C33%2C0%2C0%2C1%2C36%2C3%22%3E%0A%20%20%20%20%3C%2Fpath%3E%0A%3C%2Fsvg%3E%0A"); } .spinner.spinner-neutral-0 { - background: url('data:image/svg+xml;charset=utf8,%3Csvg%20id%3D%22Layer_2%22%20data-name%3D%22Layer%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-miterlimit%3A%2010%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-width%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23transparent%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-opacity%3A%201%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23ffffff%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3EPreloader_72x2%3C%2Ftitle%3E%0A%20%20%20%20%3Ccircle%20class%3D%22cls-1%22%20cx%3D%2236%22%20cy%3D%2236%22%20r%3D%2233%22%2F%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-2%22%20d%3D%22M14.3%2C60.9A33%2C33%2C0%2C0%2C1%2C36%2C3%22%3E%0A%20%20%20%20%3C%2Fpath%3E%0A%3C%2Fsvg%3E%0A'); + background: url("data:image/svg+xml;charset=utf8,%3Csvg%20id%3D%22Layer_2%22%20data-name%3D%22Layer%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-miterlimit%3A%2010%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-width%3A%205px%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23transparent%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke-opacity%3A%201%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20stroke%3A%20%23ffffff%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3EPreloader_72x2%3C%2Ftitle%3E%0A%20%20%20%20%3Ccircle%20class%3D%22cls-1%22%20cx%3D%2236%22%20cy%3D%2236%22%20r%3D%2233%22%2F%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-2%22%20d%3D%22M14.3%2C60.9A33%2C33%2C0%2C0%2C1%2C36%2C3%22%3E%0A%20%20%20%20%3C%2Fpath%3E%0A%3C%2Fsvg%3E%0A"); } .spinner.spinner-check { - -webkit-animation: none; - animation: none; - background: url('data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%220%200%2036%2036%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20focusable%3D%22false%22%20aria-hidden%3D%22true%22%20role%3D%22img%22%3E%3Cpath%20fill%3D%22%230072a3%22%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M13.72%2C27.69%2C3.29%2C17.27a1%2C1%2C0%2C0%2C1%2C1.41-1.41l9%2C9L31.29%2C7.29a1%2C1%2C0%2C0%2C1%2C1.41%2C1.41Z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E'); + -webkit-animation: none; + animation: none; + background: url("data:image/svg+xml;charset=utf8,%3Csvg%20version%3D%221.1%22%20viewBox%3D%220%200%2036%2036%22%20preserveAspectRatio%3D%22xMidYMid%20meet%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20focusable%3D%22false%22%20aria-hidden%3D%22true%22%20role%3D%22img%22%3E%3Cpath%20fill%3D%22%230072a3%22%20class%3D%22clr-i-outline%20clr-i-outline-path-1%22%20d%3D%22M13.72%2C27.69%2C3.29%2C17.27a1%2C1%2C0%2C0%2C1%2C1.41-1.41l9%2C9L31.29%2C7.29a1%2C1%2C0%2C0%2C1%2C1.41%2C1.41Z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E"); } .alert-app-level .alert-item .btn .spinner, .btn-sm .spinner { - height: 0.65rem; - width: 0.65rem; - min-height: 0.65rem; - min-width: 0.65rem; + height: 0.65rem; + width: 0.65rem; + min-height: 0.65rem; + min-width: 0.65rem; } @-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0); - transform: rotate(0); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } @keyframes spin { - 0% { - -webkit-transform: rotate(0); - transform: rotate(0); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } :root { - --clr-table-bgcolor: var(--clr-color-neutral-0); - --clr-thead-bgcolor: var(--clr-color-neutral-50); - --clr-table-header-border-bottom-color: var(--clr-color-neutral-400); - --clr-table-footer-border-top-color: var(--clr-color-neutral-400); - --clr-table-border-color: var(--clr-color-neutral-400); - --clr-tablerow-bordercolor: var(--clr-color-neutral-200); - --clr-table-border-radius: var(--clr-global-borderradius); - --clr-table-borderwidth: var(--clr-global-borderwidth); - --clr-table-cornercellradius: 0.1rem; - --clr-table-font-color: var(--clr-color-neutral-700); - --clr-thead-color: #666666; + --clr-table-bgcolor: var(--clr-color-neutral-0); + --clr-thead-bgcolor: var(--clr-color-neutral-50); + --clr-table-header-border-bottom-color: var(--clr-color-neutral-400); + --clr-table-footer-border-top-color: var(--clr-color-neutral-400); + --clr-table-border-color: var(--clr-color-neutral-400); + --clr-tablerow-bordercolor: var(--clr-color-neutral-200); + --clr-table-border-radius: var(--clr-global-borderradius); + --clr-table-borderwidth: var(--clr-global-borderwidth); + --clr-table-cornercellradius: 0.1rem; + --clr-table-font-color: var(--clr-color-neutral-700); + --clr-thead-color: #666666; } .table { - border-collapse: separate; - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-table-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-table-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-table-border-radius, 0.15rem); - background-color: #fff; - background-color: var(--clr-table-bgcolor, #fff); - color: #666; - color: var(--clr-table-font-color, #666); - margin: 0; - margin-top: 1.2rem; - max-width: 100%; - width: 100%; + border-collapse: separate; + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-table-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-table-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-table-border-radius, 0.15rem); + background-color: #fff; + background-color: var(--clr-table-bgcolor, #fff); + color: #666; + color: var(--clr-table-font-color, #666); + margin: 0; + margin-top: 1.2rem; + max-width: 100%; + width: 100%; } .table td, .table th { - font-size: 0.65rem; - line-height: 0.7rem; - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var(--clr-table-borderwidth, 0.05rem); - border-top-color: #e8e8e8; - border-top-color: var(--clr-tablerow-bordercolor, #e8e8e8); - padding: 0.55rem 0.6rem 0.55rem; - text-align: center; - vertical-align: top; + font-size: 0.65rem; + line-height: 0.7rem; + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var(--clr-table-borderwidth, 0.05rem); + border-top-color: #e8e8e8; + border-top-color: var(--clr-tablerow-bordercolor, #e8e8e8); + padding: 0.55rem 0.6rem 0.55rem; + text-align: center; + vertical-align: top; } .table td.left, .table th.left { - text-align: left; + text-align: left; } .table td.left:first-child, .table th.left:first-child { - padding-left: 0.3rem; + padding-left: 0.3rem; } .table th { - color: #666; - color: var(--clr-thead-color, #666); - font-size: 0.55rem; - font-weight: 600; - letter-spacing: 0.03em; - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); - vertical-align: bottom; - border-bottom-style: solid; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-table-borderwidth, 0.05rem); - border-bottom-color: #ccc; - border-bottom-color: var(--clr-table-border-color, #ccc); - border-top: 0 none; + color: #666; + color: var(--clr-thead-color, #666); + font-size: 0.55rem; + font-weight: 600; + letter-spacing: 0.03em; + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); + vertical-align: bottom; + border-bottom-style: solid; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-table-borderwidth, 0.05rem); + border-bottom-color: #ccc; + border-bottom-color: var(--clr-table-border-color, #ccc); + border-top: 0 none; } .table tbody tr:first-child td { - border-top: 0 none; + border-top: 0 none; } .table tbody + tbody { - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var(--clr-table-borderwidth, 0.05rem); - border-top-color: #ccc; - border-top-color: var(--clr-table-border-color, #ccc); + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var(--clr-table-borderwidth, 0.05rem); + border-top-color: #ccc; + border-top-color: var(--clr-table-border-color, #ccc); } .table thead th:first-child { - border-radius: 0; - border-top-left-radius: 0.1rem; - border-top-left-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-top-left-radius: 0.1rem; + border-top-left-radius: var(--clr-table-cornercellradius, 0.1rem); } .table thead th:last-child { - border-radius: 0; - border-top-right-radius: 0.1rem; - border-top-right-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-top-right-radius: 0.1rem; + border-top-right-radius: var(--clr-table-cornercellradius, 0.1rem); } .table tbody:last-child tr:last-child td:first-child { - border-radius: 0; - border-bottom-left-radius: 0.1rem; - border-bottom-left-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-bottom-left-radius: 0.1rem; + border-bottom-left-radius: var(--clr-table-cornercellradius, 0.1rem); } .table tbody:last-child tr:last-child td:last-child { - border-radius: 0; - border-bottom-right-radius: 0.1rem; - border-bottom-right-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-bottom-right-radius: 0.1rem; + border-bottom-right-radius: var(--clr-table-cornercellradius, 0.1rem); } .table-compact td, .table-compact th { - padding-top: 0.3rem; - padding-bottom: 0.25rem; + padding-top: 0.3rem; + padding-bottom: 0.25rem; } .table.table-vertical thead th { - border: 0 none; - border-radius: 0; - display: none; + border: 0 none; + border-radius: 0; + display: none; } .table.table-vertical th { - border-bottom: 0; - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var(--clr-table-borderwidth, 0.05rem); - border-top-color: #ccc; - border-top-color: var(--clr-table-border-color, #ccc); - vertical-align: top; + border-bottom: 0; + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var(--clr-table-borderwidth, 0.05rem); + border-top-color: #ccc; + border-top-color: var(--clr-table-border-color, #ccc); + vertical-align: top; } .table.table-vertical td, .table.table-vertical th { - text-align: left; - border-color: #ccc; - border-color: var(--clr-table-border-color, #ccc); + text-align: left; + border-color: #ccc; + border-color: var(--clr-table-border-color, #ccc); } .table.table-vertical td:first-child, .table.table-vertical th:first-child { - border-right-style: solid; - border-right-width: 0.05rem; - border-right-width: var(--clr-table-borderwidth, 0.05rem); - border-right-color: #ccc; - border-right-color: var(--clr-table-border-color, #ccc); - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); - font-weight: 600; + border-right-style: solid; + border-right-width: 0.05rem; + border-right-width: var(--clr-table-borderwidth, 0.05rem); + border-right-color: #ccc; + border-right-color: var(--clr-table-border-color, #ccc); + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); + font-weight: 600; } .table.table-vertical tbody:first-of-type tr:first-child td, .table.table-vertical tbody:first-of-type tr:first-child th { - border-top: 0 none; + border-top: 0 none; } .table.table-vertical tbody:first-of-type tr:first-child td:first-child, .table.table-vertical tbody:first-of-type tr:first-child th:first-child { - border-radius: 0; - border-top-left-radius: 0.1rem; - border-top-left-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-top-left-radius: 0.1rem; + border-top-left-radius: var(--clr-table-cornercellradius, 0.1rem); } .table.table-vertical tbody:first-of-type tr:first-child td:last-child, .table.table-vertical tbody:first-of-type tr:first-child th:last-child { - border-radius: 0; - border-top-right-radius: 0.1rem; - border-top-right-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-top-right-radius: 0.1rem; + border-top-right-radius: var(--clr-table-cornercellradius, 0.1rem); } .table.table-vertical tbody:last-child tr:last-child td:first-child, .table.table-vertical tbody:last-child tr:last-child th:first-child { - border-radius: 0; - border-bottom-left-radius: 0.1rem; - border-bottom-left-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-bottom-left-radius: 0.1rem; + border-bottom-left-radius: var(--clr-table-cornercellradius, 0.1rem); } .table.table-vertical tbody:last-child tr:last-child td:last-child, .table.table-vertical tbody:last-child tr:last-child th:last-child { - border-radius: 0; - border-bottom-right-radius: 0.1rem; - border-bottom-right-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-bottom-right-radius: 0.1rem; + border-bottom-right-radius: var(--clr-table-cornercellradius, 0.1rem); } .table.table-noborder { - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background-color: transparent; - border: 0; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background-color: transparent; + border: 0; } .table.table-noborder th { - background-color: transparent; - border-bottom-color: #ccc; - border-bottom-color: var(--clr-table-border-color, #ccc); - border-top: 0 none; + background-color: transparent; + border-bottom-color: #ccc; + border-bottom-color: var(--clr-table-border-color, #ccc); + border-top: 0 none; } .table.table-noborder th:first-child { - border-right: 0 none; + border-right: 0 none; } .table.table-noborder td { - border-top: 0 none; - padding-top: 0.6rem; + border-top: 0 none; + padding-top: 0.6rem; } .table.table-noborder td:first-child { - border-right: 0 none; + border-right: 0 none; } .table.table-noborder thead th:first-child, .table.table-noborder thead th:last-child { - border-radius: 0; + border-radius: 0; } .table.table-noborder td, .table.table-noborder th { - border-radius: 0 !important; + border-radius: 0 !important; } .table.table-noborder td:first-child, .table.table-noborder th:first-child { - padding-left: 0; + padding-left: 0; } .table.table-compact td, .table.table-compact th { - padding-top: 0.3rem; - padding-bottom: 0.25rem; + padding-top: 0.3rem; + padding-bottom: 0.25rem; } .table.table-compact.table-noborder td, .table.table-compact.table-noborder th { - padding-top: 0.35rem; - padding-bottom: 0.3rem; + padding-top: 0.35rem; + padding-bottom: 0.3rem; } :root { - --clr-tooltip-background-color: var(--clr-color-neutral-1000); - --clr-tooltip-border-radius: var(--clr-global-borderradius); - --clr-tooltip-color: var(--clr-color-neutral-0); - --clr-tooltip-font-weight: var(--clr-p3-font-weight); + --clr-tooltip-background-color: var(--clr-color-neutral-1000); + --clr-tooltip-border-radius: var(--clr-global-borderradius); + --clr-tooltip-color: var(--clr-color-neutral-0); + --clr-tooltip-font-weight: var(--clr-p3-font-weight); } .tooltip { - display: inline-block; - position: relative; - text-align: left; - overflow: visible; + display: inline-block; + position: relative; + text-align: left; + overflow: visible; } .tooltip > .tooltip-content { - visibility: hidden; - opacity: 0; - -webkit-transition: opacity 0.3s linear; - transition: opacity 0.3s linear; - white-space: normal; - z-index: 1070; + visibility: hidden; + opacity: 0; + -webkit-transition: opacity 0.3s linear; + transition: opacity 0.3s linear; + white-space: normal; + z-index: 1070; } .tooltip:focus > .tooltip-content, .tooltip:hover > .tooltip-content { - visibility: visible; - opacity: 1; + visibility: visible; + opacity: 1; } .tooltip:focus > .tooltip-content:empty, .tooltip:hover > .tooltip-content:empty { - visibility: hidden; - opacity: 0; + visibility: hidden; + opacity: 0; } .tooltip:focus { - outline: 0; + outline: 0; } .tooltip:focus > :first-child { - outline-offset: 0.05rem; - outline-width: 0.05rem; - outline-color: #3b99fc; - outline-style: solid; + outline-offset: 0.05rem; + outline-width: 0.05rem; + outline-color: #3b99fc; + outline-style: solid; } .tooltip .tooltip-content.tooltip-top-right, .tooltip.tooltip-top-right > .tooltip-content, .tooltip > .tooltip-content { - color: #fff; - color: var(--clr-tooltip-color, #fff); - font-size: 0.65rem; - font-weight: 400; - font-weight: var(--clr-tooltip-font-weight, 400); - letter-spacing: normal; - background-color: #000; - background-color: var(--clr-tooltip-background-color, #000); - border-radius: 0.15rem; - border-radius: var(--clr-tooltip-border-radius, 0.15rem); - line-height: 0.9rem; - margin: 0; - padding: 0.45rem 0.6rem; - width: 12rem; - position: absolute; - top: auto; - bottom: 100%; - left: 50%; - right: auto; - border-bottom-left-radius: 0; - margin-bottom: 0.8rem; + color: #fff; + color: var(--clr-tooltip-color, #fff); + font-size: 0.65rem; + font-weight: 400; + font-weight: var(--clr-tooltip-font-weight, 400); + letter-spacing: normal; + background-color: #000; + background-color: var(--clr-tooltip-background-color, #000); + border-radius: 0.15rem; + border-radius: var(--clr-tooltip-border-radius, 0.15rem); + line-height: 0.9rem; + margin: 0; + padding: 0.45rem 0.6rem; + width: 12rem; + position: absolute; + top: auto; + bottom: 100%; + left: 50%; + right: auto; + border-bottom-left-radius: 0; + margin-bottom: 0.8rem; } .tooltip .tooltip-content.tooltip-top-right::before, .tooltip.tooltip-top-right > .tooltip-content::before, .tooltip > .tooltip-content::before { - position: absolute; - bottom: -0.42rem; - left: 0; - top: auto; - right: auto; - content: ''; - border-left: 0.3rem solid #000; - border-left-color: var(--clr-tooltip-background-color); - border-top: 0.25rem solid #000; - border-top-color: var(--clr-tooltip-background-color); - border-right: 0.3rem solid transparent; - border-bottom: 0.25rem solid transparent; + position: absolute; + bottom: -0.42rem; + left: 0; + top: auto; + right: auto; + content: ""; + border-left: 0.3rem solid #000; + border-left-color: var(--clr-tooltip-background-color); + border-top: 0.25rem solid #000; + border-top-color: var(--clr-tooltip-background-color); + border-right: 0.3rem solid transparent; + border-bottom: 0.25rem solid transparent; } .tooltip .tooltip-content.tooltip-top-left, .tooltip.tooltip-top-left > .tooltip-content { - color: #fff; - color: var(--clr-tooltip-color, #fff); - font-size: 0.65rem; - font-weight: 400; - font-weight: var(--clr-tooltip-font-weight, 400); - letter-spacing: normal; - background-color: #000; - background-color: var(--clr-tooltip-background-color, #000); - border-radius: 0.15rem; - border-radius: var(--clr-tooltip-border-radius, 0.15rem); - line-height: 0.9rem; - margin: 0; - padding: 0.45rem 0.6rem; - width: 12rem; - position: absolute; - top: auto; - bottom: 100%; - right: 50%; - left: auto; - border-bottom-right-radius: 0; - margin-bottom: 0.8rem; + color: #fff; + color: var(--clr-tooltip-color, #fff); + font-size: 0.65rem; + font-weight: 400; + font-weight: var(--clr-tooltip-font-weight, 400); + letter-spacing: normal; + background-color: #000; + background-color: var(--clr-tooltip-background-color, #000); + border-radius: 0.15rem; + border-radius: var(--clr-tooltip-border-radius, 0.15rem); + line-height: 0.9rem; + margin: 0; + padding: 0.45rem 0.6rem; + width: 12rem; + position: absolute; + top: auto; + bottom: 100%; + right: 50%; + left: auto; + border-bottom-right-radius: 0; + margin-bottom: 0.8rem; } .tooltip .tooltip-content.tooltip-top-left::before, .tooltip.tooltip-top-left > .tooltip-content::before { - position: absolute; - bottom: -0.42rem; - right: 0; - top: auto; - left: auto; - content: ''; - border-right: 0.3rem solid #000; - border-right-color: var(--clr-tooltip-background-color); - border-top: 0.25rem solid #000; - border-top-color: var(--clr-tooltip-background-color); - border-left: 0.3rem solid transparent; - border-bottom: 0.25rem solid transparent; + position: absolute; + bottom: -0.42rem; + right: 0; + top: auto; + left: auto; + content: ""; + border-right: 0.3rem solid #000; + border-right-color: var(--clr-tooltip-background-color); + border-top: 0.25rem solid #000; + border-top-color: var(--clr-tooltip-background-color); + border-left: 0.3rem solid transparent; + border-bottom: 0.25rem solid transparent; } .tooltip .tooltip-content.tooltip-bottom-right, .tooltip.tooltip-bottom-right > .tooltip-content { - color: #fff; - color: var(--clr-tooltip-color, #fff); - font-size: 0.65rem; - font-weight: 400; - font-weight: var(--clr-tooltip-font-weight, 400); - letter-spacing: normal; - background-color: #000; - background-color: var(--clr-tooltip-background-color, #000); - border-radius: 0.15rem; - border-radius: var(--clr-tooltip-border-radius, 0.15rem); - line-height: 0.9rem; - margin: 0; - padding: 0.45rem 0.6rem; - width: 12rem; - position: absolute; - bottom: auto; - top: 100%; - left: 50%; - right: auto; - border-top-left-radius: 0; - margin-top: 0.8rem; + color: #fff; + color: var(--clr-tooltip-color, #fff); + font-size: 0.65rem; + font-weight: 400; + font-weight: var(--clr-tooltip-font-weight, 400); + letter-spacing: normal; + background-color: #000; + background-color: var(--clr-tooltip-background-color, #000); + border-radius: 0.15rem; + border-radius: var(--clr-tooltip-border-radius, 0.15rem); + line-height: 0.9rem; + margin: 0; + padding: 0.45rem 0.6rem; + width: 12rem; + position: absolute; + bottom: auto; + top: 100%; + left: 50%; + right: auto; + border-top-left-radius: 0; + margin-top: 0.8rem; } .tooltip .tooltip-content.tooltip-bottom-right::before, .tooltip.tooltip-bottom-right > .tooltip-content::before { - position: absolute; - top: -0.42rem; - left: 0; - bottom: auto; - right: auto; - content: ''; - border-left: 0.3rem solid #000; - border-left-color: var(--clr-tooltip-background-color); - border-bottom: 0.25rem solid #000; - border-bottom-color: var(--clr-tooltip-background-color); - border-right: 0.3rem solid transparent; - border-top: 0.25rem solid transparent; + position: absolute; + top: -0.42rem; + left: 0; + bottom: auto; + right: auto; + content: ""; + border-left: 0.3rem solid #000; + border-left-color: var(--clr-tooltip-background-color); + border-bottom: 0.25rem solid #000; + border-bottom-color: var(--clr-tooltip-background-color); + border-right: 0.3rem solid transparent; + border-top: 0.25rem solid transparent; } .tooltip .tooltip-content.tooltip-bottom-left, .tooltip.tooltip-bottom-left > .tooltip-content { - color: #fff; - color: var(--clr-tooltip-color, #fff); - font-size: 0.65rem; - font-weight: 400; - font-weight: var(--clr-tooltip-font-weight, 400); - letter-spacing: normal; - background-color: #000; - background-color: var(--clr-tooltip-background-color, #000); - border-radius: 0.15rem; - border-radius: var(--clr-tooltip-border-radius, 0.15rem); - line-height: 0.9rem; - margin: 0; - padding: 0.45rem 0.6rem; - width: 12rem; - position: absolute; - bottom: auto; - top: 100%; - right: 50%; - left: auto; - border-top-right-radius: 0; - margin-top: 0.8rem; + color: #fff; + color: var(--clr-tooltip-color, #fff); + font-size: 0.65rem; + font-weight: 400; + font-weight: var(--clr-tooltip-font-weight, 400); + letter-spacing: normal; + background-color: #000; + background-color: var(--clr-tooltip-background-color, #000); + border-radius: 0.15rem; + border-radius: var(--clr-tooltip-border-radius, 0.15rem); + line-height: 0.9rem; + margin: 0; + padding: 0.45rem 0.6rem; + width: 12rem; + position: absolute; + bottom: auto; + top: 100%; + right: 50%; + left: auto; + border-top-right-radius: 0; + margin-top: 0.8rem; } .tooltip .tooltip-content.tooltip-bottom-left::before, .tooltip.tooltip-bottom-left > .tooltip-content::before { - position: absolute; - top: -0.42rem; - right: 0; - bottom: auto; - left: auto; - content: ''; - border-right: 0.3rem solid #000; - border-right-color: var(--clr-tooltip-background-color); - border-bottom: 0.25rem solid #000; - border-bottom-color: var(--clr-tooltip-background-color); - border-left: 0.3rem solid transparent; - border-top: 0.25rem solid transparent; + position: absolute; + top: -0.42rem; + right: 0; + bottom: auto; + left: auto; + content: ""; + border-right: 0.3rem solid #000; + border-right-color: var(--clr-tooltip-background-color); + border-bottom: 0.25rem solid #000; + border-bottom-color: var(--clr-tooltip-background-color); + border-left: 0.3rem solid transparent; + border-top: 0.25rem solid transparent; } .tooltip .tooltip-content.tooltip-right, .tooltip.tooltip-right > .tooltip-content { - position: absolute; - right: auto; - left: 100%; - top: 50%; - bottom: auto; - color: #fff; - color: var(--clr-tooltip-color, #fff); - font-size: 0.65rem; - font-weight: 400; - font-weight: var(--clr-tooltip-font-weight, 400); - letter-spacing: normal; - background-color: #000; - background-color: var(--clr-tooltip-background-color, #000); - border-radius: 0.15rem; - border-radius: var(--clr-tooltip-border-radius, 0.15rem); - line-height: 0.9rem; - margin: 0; - padding: 0.45rem 0.6rem; - width: 12rem; - border-top-left-radius: 0; - margin-left: 0.8rem; + position: absolute; + right: auto; + left: 100%; + top: 50%; + bottom: auto; + color: #fff; + color: var(--clr-tooltip-color, #fff); + font-size: 0.65rem; + font-weight: 400; + font-weight: var(--clr-tooltip-font-weight, 400); + letter-spacing: normal; + background-color: #000; + background-color: var(--clr-tooltip-background-color, #000); + border-radius: 0.15rem; + border-radius: var(--clr-tooltip-border-radius, 0.15rem); + line-height: 0.9rem; + margin: 0; + padding: 0.45rem 0.6rem; + width: 12rem; + border-top-left-radius: 0; + margin-left: 0.8rem; } .tooltip .tooltip-content.tooltip-right::before, .tooltip.tooltip-right > .tooltip-content::before { - position: absolute; - top: 0; - left: -0.45rem; - bottom: auto; - right: auto; - content: ''; - border-top: 0.3rem solid #000; - border-top-color: var(--clr-tooltip-background-color); - border-right: 0.25rem solid #000; - border-right-color: var(--clr-tooltip-background-color); - border-bottom: 0.3rem solid transparent; - border-left: 0.25rem solid transparent; + position: absolute; + top: 0; + left: -0.45rem; + bottom: auto; + right: auto; + content: ""; + border-top: 0.3rem solid #000; + border-top-color: var(--clr-tooltip-background-color); + border-right: 0.25rem solid #000; + border-right-color: var(--clr-tooltip-background-color); + border-bottom: 0.3rem solid transparent; + border-left: 0.25rem solid transparent; } .tooltip .tooltip-content.tooltip-left, .tooltip.tooltip-left > .tooltip-content { - position: absolute; - left: auto; - right: 100%; - top: 50%; - bottom: auto; - color: #fff; - color: var(--clr-tooltip-color, #fff); - font-size: 0.65rem; - font-weight: 400; - font-weight: var(--clr-tooltip-font-weight, 400); - letter-spacing: normal; - background-color: #000; - background-color: var(--clr-tooltip-background-color, #000); - border-radius: 0.15rem; - border-radius: var(--clr-tooltip-border-radius, 0.15rem); - line-height: 0.9rem; - margin: 0; - padding: 0.45rem 0.6rem; - width: 12rem; - border-top-right-radius: 0; - margin-right: 0.8rem; + position: absolute; + left: auto; + right: 100%; + top: 50%; + bottom: auto; + color: #fff; + color: var(--clr-tooltip-color, #fff); + font-size: 0.65rem; + font-weight: 400; + font-weight: var(--clr-tooltip-font-weight, 400); + letter-spacing: normal; + background-color: #000; + background-color: var(--clr-tooltip-background-color, #000); + border-radius: 0.15rem; + border-radius: var(--clr-tooltip-border-radius, 0.15rem); + line-height: 0.9rem; + margin: 0; + padding: 0.45rem 0.6rem; + width: 12rem; + border-top-right-radius: 0; + margin-right: 0.8rem; } .tooltip .tooltip-content.tooltip-left::before, .tooltip.tooltip-left > .tooltip-content::before { - position: absolute; - top: 0; - right: -0.45rem; - bottom: auto; - left: auto; - content: ''; - border-top: 0.3rem solid #000; - border-top-color: var(--clr-tooltip-background-color); - border-left: 0.25rem solid #000; - border-left-color: var(--clr-tooltip-background-color); - border-bottom: 0.3rem solid transparent; - border-right: 0.25rem solid transparent; + position: absolute; + top: 0; + right: -0.45rem; + bottom: auto; + left: auto; + content: ""; + border-top: 0.3rem solid #000; + border-top-color: var(--clr-tooltip-background-color); + border-left: 0.25rem solid #000; + border-left-color: var(--clr-tooltip-background-color); + border-bottom: 0.3rem solid transparent; + border-right: 0.25rem solid transparent; } .tooltip .tooltip-content.tooltip-xs, .tooltip.tooltip-xs > .tooltip-content { - width: 3.6rem; + width: 3.6rem; } .tooltip .tooltip-content.tooltip-sm, .tooltip.tooltip-sm > .tooltip-content { - width: 6rem; + width: 6rem; } .tooltip .tooltip-content.tooltip-md, .tooltip.tooltip-md > .tooltip-content { - width: 12rem; + width: 12rem; } .tooltip .tooltip-content.tooltip-lg, .tooltip.tooltip-lg > .tooltip-content { - width: 18rem; + width: 18rem; } .tooltip.tooltip-top-left > .btn + .tooltip-content, .tooltip.tooltip-top-right > .btn + .tooltip-content, .tooltip > .btn + .tooltip-content { - margin-bottom: 0.5rem; + margin-bottom: 0.5rem; } .tooltip.tooltip-bottom-left > .btn + .tooltip-content, .tooltip.tooltip-bottom-right > .btn + .tooltip-content { - margin-top: 0.5rem; + margin-top: 0.5rem; } .tooltip.tooltip-right > .btn + .tooltip-content { - margin-left: 0.2rem; + margin-left: 0.2rem; } .tooltip > .clr-icon { - margin-right: 0; + margin-right: 0; } .tooltip cds-icon > svg { - pointer-events: none; + pointer-events: none; } .tooltip-trigger:focus + .tooltip-content { - visibility: visible; + visibility: visible; } :root { - --clr-form-disabled-background-color: var(--clr-color-neutral-400); - --clr-forms-label-color: var(--clr-color-neutral-800); - --clr-forms-text-color: var(--clr-color-neutral-1000); - --clr-forms-invalid-color: var(--clr-color-danger-800); - --clr-forms-valid-color: var(--clr-color-success-700); - --clr-forms-valid-text-color: var(--clr-color-success-900); - --clr-forms-subtext-color: var(--clr-color-neutral-600); - --clr-forms-placeholder-color: var(--clr-color-neutral-600); - --clr-forms-border-color: var(--clr-color-neutral-500); - --clr-forms-focused-color: var(--clr-color-action-600); - --clr-forms-subtext-disabled-color: #8c8c8c; - --clr-forms-border-disabled-color: #b3b3b3; - --clr-forms-text-disabled-color: #b3b3b3; - --clr-forms-label-disabled-color: #8c8c8c; - --clr-forms-label-font-weight: var(--clr-font-weight-bold); - --clr-forms-block-label-font-weight: 400; - --clr-forms-text-font-weight: 400; - --clr-forms-textarea-background-color: var(--clr-color-neutral-0); - --clr-forms-textarea-focused-outline: 0 0 0.1rem 0.1rem #69c0e2; - --clr-forms-textarea-invalid-focused-outline: 0 0 0.1rem 0.1rem #ff745c; - --clr-forms-select-hover-background: rgba(222, 222, 222, 0.5); - --clr-forms-select-caret-hover-color: var(--clr-color-neutral-600); - --clr-forms-select-caret-color: var(--clr-color-neutral-500); - --clr-forms-select-option-color: var(--clr-forms-text-color); - --clr-forms-select-multiple-background-color: var( - --clr-forms-textarea-background-color - ); - --clr-forms-select-multiple-border-color: var(--clr-color-neutral-400); - --clr-forms-select-multiple-option-color: var(--clr-forms-text-color); - --clr-forms-select-multiple-error-focus-color: #ff745c; - --clr-forms-checkbox-label-color: var(--clr-forms-label-color); - --clr-forms-checkbox-background-color: var(--clr-color-action-600); - --clr-forms-checkbox-indeterminate-border-color: var( - --clr-color-action-600 - ); - --clr-forms-checkbox-mark-color: var(--clr-color-neutral-0); - --clr-forms-checkbox-disabled-background-color: var( - --clr-form-disabled-background-color - ); - --clr-forms-checkbox-disabled-mark-color: var(--clr-color-neutral-1000); - --clr-forms-checkbox-border-radius: var(--clr-global-borderradius); - --clr-forms-checkbox-checked-shadow: inset 0 0 0 0.3rem - var(--clr-forms-checkbox-background-color); - --clr-forms-radio-label-color: var(--clr-forms-checkbox-label-color); - --clr-forms-radio-disabled-background-color: var( - --clr-form-disabled-background-color - ); - --clr-forms-radio-disabled-mark-color: var( - --clr-forms-checkbox-disabled-mark-color - ); - --clr-forms-radio-selected-shadow: var(--clr-forms-checkbox-checked-shadow); - --clr-forms-radio-disabled-shadow: var(--clr-forms-checkbox-checked-shadow); - --clr-forms-radio-focused-shadow: 0 0 0.1rem 0.1rem #69c0e2; - --clr-forms-range-progress-fill-color: var(--clr-color-action-600); - --clr-forms-range-track-color: var(--clr-color-neutral-200); + --clr-form-disabled-background-color: var(--clr-color-neutral-400); + --clr-forms-label-color: var(--clr-color-neutral-800); + --clr-forms-text-color: var(--clr-color-neutral-1000); + --clr-forms-invalid-color: var(--clr-color-danger-800); + --clr-forms-valid-color: var(--clr-color-success-700); + --clr-forms-valid-text-color: var(--clr-color-success-900); + --clr-forms-subtext-color: var(--clr-color-neutral-600); + --clr-forms-placeholder-color: var(--clr-color-neutral-600); + --clr-forms-border-color: var(--clr-color-neutral-500); + --clr-forms-focused-color: var(--clr-color-action-600); + --clr-forms-subtext-disabled-color: #8c8c8c; + --clr-forms-border-disabled-color: #b3b3b3; + --clr-forms-text-disabled-color: #b3b3b3; + --clr-forms-label-disabled-color: #8c8c8c; + --clr-forms-label-font-weight: var(--clr-font-weight-bold); + --clr-forms-block-label-font-weight: 400; + --clr-forms-text-font-weight: 400; + --clr-forms-textarea-background-color: var(--clr-color-neutral-0); + --clr-forms-textarea-focused-outline: 0 0 0.1rem 0.1rem #69c0e2; + --clr-forms-textarea-invalid-focused-outline: 0 0 0.1rem 0.1rem #ff745c; + --clr-forms-select-hover-background: rgba(222, 222, 222, 0.5); + --clr-forms-select-caret-hover-color: var(--clr-color-neutral-600); + --clr-forms-select-caret-color: var(--clr-color-neutral-500); + --clr-forms-select-option-color: var(--clr-forms-text-color); + --clr-forms-select-multiple-background-color: var( + --clr-forms-textarea-background-color + ); + --clr-forms-select-multiple-border-color: var(--clr-color-neutral-400); + --clr-forms-select-multiple-option-color: var(--clr-forms-text-color); + --clr-forms-select-multiple-error-focus-color: #ff745c; + --clr-forms-checkbox-label-color: var(--clr-forms-label-color); + --clr-forms-checkbox-background-color: var(--clr-color-action-600); + --clr-forms-checkbox-indeterminate-border-color: var(--clr-color-action-600); + --clr-forms-checkbox-mark-color: var(--clr-color-neutral-0); + --clr-forms-checkbox-disabled-background-color: var( + --clr-form-disabled-background-color + ); + --clr-forms-checkbox-disabled-mark-color: var(--clr-color-neutral-1000); + --clr-forms-checkbox-border-radius: var(--clr-global-borderradius); + --clr-forms-checkbox-checked-shadow: inset 0 0 0 0.3rem + var(--clr-forms-checkbox-background-color); + --clr-forms-radio-label-color: var(--clr-forms-checkbox-label-color); + --clr-forms-radio-disabled-background-color: var( + --clr-form-disabled-background-color + ); + --clr-forms-radio-disabled-mark-color: var( + --clr-forms-checkbox-disabled-mark-color + ); + --clr-forms-radio-selected-shadow: var(--clr-forms-checkbox-checked-shadow); + --clr-forms-radio-disabled-shadow: var(--clr-forms-checkbox-checked-shadow); + --clr-forms-radio-focused-shadow: 0 0 0.1rem 0.1rem #69c0e2; + --clr-forms-range-progress-fill-color: var(--clr-color-action-600); + --clr-forms-range-track-color: var(--clr-color-neutral-200); } .clr-date-container .clr-input-wrapper { - max-width: -webkit-fit-content; - max-width: -moz-fit-content; - max-width: fit-content; + max-width: -webkit-fit-content; + max-width: -moz-fit-content; + max-width: fit-content; } .clr-form-control { - margin-top: 1.2rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; + margin-top: 1.2rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; } .clr-form-control-disabled { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); } .clr-form-control-disabled .clr-control-label, .clr-form-control-disabled label { - color: #8c8c8c; - color: var(--clr-forms-label-disabled-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-label-disabled-color, #8c8c8c); } .clr-form-control-disabled .clr-input, .clr-form-control-disabled .clr-select, .clr-form-control-disabled .clr-textarea { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - border-bottom-color: #b3b3b3; - border-bottom-color: var(--clr-forms-border-disabled-color, #b3b3b3); + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + border-bottom-color: #b3b3b3; + border-bottom-color: var(--clr-forms-border-disabled-color, #b3b3b3); } -.clr-form-control-disabled input[type='range']::-webkit-slider-thumb { - background-color: #b3b3b3; - background-color: var(--clr-forms-border-disabled-color, #b3b3b3); +.clr-form-control-disabled input[type="range"]::-webkit-slider-thumb { + background-color: #b3b3b3; + background-color: var(--clr-forms-border-disabled-color, #b3b3b3); } .clr-form-control-disabled .clr-subtext { - color: #8c8c8c; - color: var(--cclr-forms-subtext-disabled-color, #8c8c8c); + color: #8c8c8c; + color: var(--cclr-forms-subtext-disabled-color, #8c8c8c); } .clr-form-control-multi { - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -ms-flex-wrap: wrap; - flex-wrap: wrap; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -ms-flex-wrap: wrap; + flex-wrap: wrap; } .clr-form-control-multi .clr-control-label { - width: 100%; + width: 100%; } .clr-control-label { - display: block; - color: #454545; - color: var(--clr-forms-label-color, #454545); - font-size: 0.65rem; - font-weight: 600; - font-weight: var(--clr-forms-label-font-weight, 600); - line-height: 0.9rem; + display: block; + color: #454545; + color: var(--clr-forms-label-color, #454545); + font-size: 0.65rem; + font-weight: 600; + font-weight: var(--clr-forms-label-font-weight, 600); + line-height: 0.9rem; } .clr-control-container { - display: inline-block; + display: inline-block; } .clr-control-inline { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -ms-flex-line-pack: start; - align-content: flex-start; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-line-pack: start; + align-content: flex-start; } .clr-control-inline .clr-control-label { - display: inline-block; - margin-right: 0.6rem; - width: auto; + display: inline-block; + margin-right: 0.6rem; + width: auto; } .clr-subtext { - display: block; - font-size: 0.55rem; - line-height: 0.6rem; - color: #8c8c8c; - color: var(--clr-forms-subtext-color, #8c8c8c); - margin-top: 0.3rem; + display: block; + font-size: 0.55rem; + line-height: 0.6rem; + color: #8c8c8c; + color: var(--clr-forms-subtext-color, #8c8c8c); + margin-top: 0.3rem; } .clr-subtext-wrapper { - -ms-flex-preferred-size: 100%; - flex-basis: 100%; - display: -webkit-box; - display: -ms-flexbox; - display: flex; + -ms-flex-preferred-size: 100%; + flex-basis: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .clr-validate-icon { - height: 1.2rem; - width: 1.2rem; - min-height: 1.2rem; - min-width: 1.2rem; - color: #c21d00; - color: var(--clr-forms-invalid-color, #c21d00); - display: none; - margin-left: -1.2rem; + height: 1.2rem; + width: 1.2rem; + min-height: 1.2rem; + min-width: 1.2rem; + color: #c21d00; + color: var(--clr-forms-invalid-color, #c21d00); + display: none; + margin-left: -1.2rem; } .clr-success .clr-input { - border-bottom-color: #3c8500; - border-bottom-color: var(--clr-forms-valid-color, #3c8500); + border-bottom-color: #3c8500; + border-bottom-color: var(--clr-forms-valid-color, #3c8500); } .clr-success .clr-validate-icon { - display: inline-block; - color: #3c8500; - color: var(--clr-forms-valid-color, #3c8500); - margin-left: -0.2rem; + display: inline-block; + color: #3c8500; + color: var(--clr-forms-valid-color, #3c8500); + margin-left: -0.2rem; } .clr-success .clr-subtext { - color: #255200; - color: var(--clr-forms-valid-text-color, #255200); + color: #255200; + color: var(--clr-forms-valid-text-color, #255200); } .clr-error .clr-validate-icon { - margin-left: -0.2rem; - display: inline-block; + margin-left: -0.2rem; + display: inline-block; } .clr-error .clr-subtext { - color: #c21d00; - color: var(--clr-forms-invalid-color, #c21d00); + color: #c21d00; + color: var(--clr-forms-invalid-color, #c21d00); } .clr-form-horizontal .clr-form-control > .clr-control-label, .clr-form-horizontal - .clr-form-control - > .clr-form-control.clr-form-control-multi - .clr-control-label { - width: 9.6rem; - -ms-flex-negative: 0; - flex-shrink: 0; - margin-top: 0.15rem; + .clr-form-control + > .clr-form-control.clr-form-control-multi + .clr-control-label { + width: 9.6rem; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-top: 0.15rem; } .clr-form-horizontal.clr-row .clr-control-label { - width: auto; + width: auto; } .clr-form-horizontal .clr-form-control { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } .clr-form-compact .clr-form-control > .clr-control-label { - width: 9.6rem; - min-width: 9.6rem; + width: 9.6rem; + min-width: 9.6rem; } .clr-form-compact .clr-form-control.clr-row > .clr-control-label { - width: auto; - min-width: auto; + width: auto; + min-width: auto; } .clr-form-compact .clr-control-label { - margin-top: 0.15rem; + margin-top: 0.15rem; } .clr-form-compact .clr-form-control { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - margin-top: 0.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + margin-top: 0.6rem; } .clr-form-compact .clr-control-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .clr-form-compact .clr-subtext { - display: inline-block; - margin-top: 0.3rem; - margin-left: 1.2rem; + display: inline-block; + margin-top: 0.3rem; + margin-left: 1.2rem; } .clr-form-compact .clr-error .clr-subtext, .clr-form-compact .clr-success .clr-subtext { - margin-left: 0; + margin-left: 0; } .clr-form-compact .clr-error .clr-validate-icon, .clr-form-compact .clr-success .clr-validate-icon { - margin-left: 0; + margin-left: 0; } .clr-form-compact .clr-subtext-wrapper { - -ms-flex-preferred-size: auto; - flex-basis: auto; - display: inline-block; + -ms-flex-preferred-size: auto; + flex-basis: auto; + display: inline-block; } .clr-form { - padding: 0.3rem; + padding: 0.3rem; } .clr-form-group { - padding: 0.3rem; + padding: 0.3rem; } .clr-checkbox-wrapper { - position: relative; + position: relative; } .clr-checkbox-wrapper .clr-control-label { - font-weight: 400; - display: block; + font-weight: 400; + display: block; } .clr-checkbox-wrapper ~ .clr-validate-icon { - margin-left: 0; -} -.clr-checkbox-wrapper ~ .clr-subtext { - display: inline-block; -} -.clr-checkbox-wrapper input[type='checkbox'] { - position: absolute; - opacity: 0; - top: 0.25rem; - left: 0; - height: 0.8rem; - width: 0.8rem; -} -.clr-checkbox-wrapper label { - position: relative; - display: inline-block; - min-height: 1.2rem; - padding-left: 1.1rem; - margin-top: 0; - cursor: pointer; - line-height: 1.2rem; - color: #454545; - color: var(--clr-forms-checkbox-label-color, #454545); -} -.clr-checkbox-wrapper input[type='checkbox'] + label::before { - position: absolute; - top: 0.2rem; - left: 0; - content: ''; - display: inline-block; - height: 0.8rem; - width: 0.8rem; - border: 0.05rem solid; - border-color: #b3b3b3; - border-color: var(--clr-forms-border-color, #b3b3b3); - border-radius: 0.15rem; - border-radius: var(--clr-forms-checkbox-border-radius, 0.15rem); -} -.clr-checkbox-wrapper input[type='checkbox']:focus + label::before { - outline: 0; - -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + margin-left: 0; } -.clr-checkbox-wrapper input[type='checkbox'] + label::after { - position: absolute; - content: ''; - display: none; - height: 0.25rem; - width: 0.4rem; - border-left: 0.1rem solid; - border-bottom: 0.1rem solid; - border-color: #fff; - border-color: var(--clr-forms-checkbox-mark-color, #fff); - top: 0.2rem; - left: 0.2rem; - -webkit-transform: translate(0, 0.2rem) rotate(-45deg); - transform: translate(0, 0.2rem) rotate(-45deg); -} -.clr-checkbox-wrapper input[type='checkbox']:checked + label::before { - background: #0072a3; - background: var(--clr-forms-checkbox-background-color, #0072a3); - border: none; +.clr-checkbox-wrapper ~ .clr-subtext { + display: inline-block; } -.clr-checkbox-wrapper input[type='checkbox']:checked + label::after { - display: inline-block; +.clr-checkbox-wrapper input[type="checkbox"] { + position: absolute; + opacity: 0; + top: 0.25rem; + left: 0; + height: 0.8rem; + width: 0.8rem; } -.clr-checkbox-wrapper input[type='checkbox'].clr-indeterminate + label::before, -.clr-checkbox-wrapper input[type='checkbox']:indeterminate + label::before { - border: 0.05rem solid; - border-color: #0072a3; - border-color: var(--clr-forms-checkbox-indeterminate-border-color, #0072a3); -} -.clr-checkbox-wrapper input[type='checkbox'].clr-indeterminate + label::after, -.clr-checkbox-wrapper input[type='checkbox']:indeterminate + label::after { - border-left: none; - border-bottom-color: #0072a3; - border-bottom-color: var( - --clr-forms-checkbox-indeterminate-border-color, - #0072a3 - ); - display: inline-block; - -webkit-transform: translate(0, 0.2rem); - transform: translate(0, 0.2rem); +.clr-checkbox-wrapper label { + position: relative; + display: inline-block; + min-height: 1.2rem; + padding-left: 1.1rem; + margin-top: 0; + cursor: pointer; + line-height: 1.2rem; + color: #454545; + color: var(--clr-forms-checkbox-label-color, #454545); +} +.clr-checkbox-wrapper input[type="checkbox"] + label::before { + position: absolute; + top: 0.2rem; + left: 0; + content: ""; + display: inline-block; + height: 0.8rem; + width: 0.8rem; + border: 0.05rem solid; + border-color: #b3b3b3; + border-color: var(--clr-forms-border-color, #b3b3b3); + border-radius: 0.15rem; + border-radius: var(--clr-forms-checkbox-border-radius, 0.15rem); +} +.clr-checkbox-wrapper input[type="checkbox"]:focus + label::before { + outline: 0; + -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + box-shadow: 0 0 0.1rem 0.1rem #69c0e2; +} +.clr-checkbox-wrapper input[type="checkbox"] + label::after { + position: absolute; + content: ""; + display: none; + height: 0.25rem; + width: 0.4rem; + border-left: 0.1rem solid; + border-bottom: 0.1rem solid; + border-color: #fff; + border-color: var(--clr-forms-checkbox-mark-color, #fff); + top: 0.2rem; + left: 0.2rem; + -webkit-transform: translate(0, 0.2rem) rotate(-45deg); + transform: translate(0, 0.2rem) rotate(-45deg); +} +.clr-checkbox-wrapper input[type="checkbox"]:checked + label::before { + background: #0072a3; + background: var(--clr-forms-checkbox-background-color, #0072a3); + border: none; +} +.clr-checkbox-wrapper input[type="checkbox"]:checked + label::after { + display: inline-block; +} +.clr-checkbox-wrapper input[type="checkbox"].clr-indeterminate + label::before, +.clr-checkbox-wrapper input[type="checkbox"]:indeterminate + label::before { + border: 0.05rem solid; + border-color: #0072a3; + border-color: var(--clr-forms-checkbox-indeterminate-border-color, #0072a3); +} +.clr-checkbox-wrapper input[type="checkbox"].clr-indeterminate + label::after, +.clr-checkbox-wrapper input[type="checkbox"]:indeterminate + label::after { + border-left: none; + border-bottom-color: #0072a3; + border-bottom-color: var( + --clr-forms-checkbox-indeterminate-border-color, + #0072a3 + ); + display: inline-block; + -webkit-transform: translate(0, 0.2rem); + transform: translate(0, 0.2rem); } .clr-checkbox-wrapper.clr-checkbox-inline { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -ms-flex-line-pack: start; - align-content: flex-start; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-line-pack: start; + align-content: flex-start; } .clr-checkbox-wrapper.clr-checkbox-inline .clr-control-label { - display: inline-block; - margin-right: 0.6rem; - width: auto; + display: inline-block; + margin-right: 0.6rem; + width: auto; } -.clr-error .clr-checkbox-wrapper input[type='checkbox'] + label::before { - border-color: #c21d00; - border-color: var(--clr-forms-invalid-color, #c21d00); +.clr-error .clr-checkbox-wrapper input[type="checkbox"] + label::before { + border-color: #c21d00; + border-color: var(--clr-forms-invalid-color, #c21d00); } .clr-form-control-disabled .clr-checkbox-wrapper label { - cursor: not-allowed; + cursor: not-allowed; } .clr-form-control-disabled - .clr-checkbox-wrapper - input[type='checkbox'] - + label::before, + .clr-checkbox-wrapper + input[type="checkbox"] + + label::before, .clr-form-control-disabled - .clr-checkbox-wrapper - input[type='checkbox']:checked - + label::before { - background-color: #ccc; - background-color: var(--clr-forms-checkbox-disabled-background-color, #ccc); - border: none; + .clr-checkbox-wrapper + input[type="checkbox"]:checked + + label::before { + background-color: #ccc; + background-color: var(--clr-forms-checkbox-disabled-background-color, #ccc); + border: none; } .clr-form-control-disabled - .clr-checkbox-wrapper - input[type='checkbox']:checked - + label::after { - border-left: 0.1rem solid; - border-bottom: 0.1rem solid; - border-left-color: #000; - border-left-color: var(--clr-forms-checkbox-disabled-mark-color, #000); - border-bottom-color: #000; - border-bottom-color: var(--clr-forms-checkbox-disabled-mark-color, #000); + .clr-checkbox-wrapper + input[type="checkbox"]:checked + + label::after { + border-left: 0.1rem solid; + border-bottom: 0.1rem solid; + border-left-color: #000; + border-left-color: var(--clr-forms-checkbox-disabled-mark-color, #000); + border-bottom-color: #000; + border-bottom-color: var(--clr-forms-checkbox-disabled-mark-color, #000); } .clr-form-control-disabled - .clr-checkbox-wrapper - input[type='checkbox']:checked.clr-indeterminate - + label::after, + .clr-checkbox-wrapper + input[type="checkbox"]:checked.clr-indeterminate + + label::after, .clr-form-control-disabled - .clr-checkbox-wrapper - input[type='checkbox']:checked:indeterminate - + label::after { - border-left: none; + .clr-checkbox-wrapper + input[type="checkbox"]:checked:indeterminate + + label::after { + border-left: none; } .clr-form-compact .clr-checkbox-wrapper { - max-width: 100%; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -ms-flex-line-pack: start; - align-content: flex-start; - height: 1.2rem; + max-width: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-line-pack: start; + align-content: flex-start; + height: 1.2rem; } .clr-form-compact .clr-checkbox-wrapper ~ .clr-subtext { - margin-left: 0.3rem; + margin-left: 0.3rem; } .clr-form-compact .clr-checkbox-wrapper .clr-control-label { - display: inline-block; - margin-right: 0.6rem; - width: auto; + display: inline-block; + margin-right: 0.6rem; + width: auto; } .clr-form-compact .clr-checkbox-wrapper .clr-control-label { - margin-top: 0; + margin-top: 0; } .clr-file-wrapper { - position: relative; - margin-top: 0.3rem; + position: relative; + margin-top: 0.3rem; } .clr-file-wrapper .clr-control-label { - font-weight: 400; - display: block; + font-weight: 400; + display: block; } .clr-file-wrapper ~ .clr-validate-icon { - margin-left: 0; + margin-left: 0; } .clr-file-wrapper ~ .clr-subtext { - display: inline-block; + display: inline-block; } .clr-file-wrapper .btn { - margin: 0; + margin: 0; } .clr-file { - height: 0; - width: 0; - opacity: 0; - overflow: hidden; - position: absolute; - z-index: -1; + height: 0; + width: 0; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; } .clr-form-compact .clr-file-wrapper { - max-width: 100%; - margin-top: 0; + max-width: 100%; + margin-top: 0; } .clr-form-compact .clr-file-wrapper ~ .clr-subtext { - margin-left: 0.3rem; + margin-left: 0.3rem; } .clr-form-compact .clr-file-wrapper ~ .clr-validate-icon { - line-height: 1.2rem; + line-height: 1.2rem; } .clr-form-compact .clr-file-wrapper ~ .clr-subtext { - line-height: 1.2rem; - margin-top: 0; + line-height: 1.2rem; + margin-top: 0; } .clr-form-compact .clr-file-wrapper .clr-control-label { - margin-top: 0; - line-height: 1.08rem; + margin-top: 0; + line-height: 1.08rem; } .clr-form-compact .clr-file-wrapper .btn { - height: 1.2rem; + height: 1.2rem; } .clr-input-wrapper { - white-space: nowrap; - max-height: 1.2rem; + white-space: nowrap; + max-height: 1.2rem; } .clr-input { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - height: 1.2rem; - color: #000; - color: var(--clr-forms-text-color, #000); - border-bottom: 0.05rem solid; - border-bottom-color: #b3b3b3; - border-bottom-color: var(--clr-forms-border-color, #b3b3b3); - display: inline-block; - padding: 0 0.3rem; - max-height: 1.2rem; - font-size: 0.65rem; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + height: 1.2rem; + color: #000; + color: var(--clr-forms-text-color, #000); + border-bottom: 0.05rem solid; + border-bottom-color: #b3b3b3; + border-bottom-color: var(--clr-forms-border-color, #b3b3b3); + display: inline-block; + padding: 0 0.3rem; + max-height: 1.2rem; + font-size: 0.65rem; } .clr-input:focus { - outline: 0; + outline: 0; } .clr-input[readonly] { - border: none; + border: none; } .clr-input:not([readonly]) { - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(95%, transparent), - color-stop(95%, var(--clr-forms-focused-color, #0072a3)) - ) - no-repeat; - background: linear-gradient( - to bottom, - transparent 95%, - var(--clr-forms-focused-color, #0072a3) 95% - ) - no-repeat; - background-size: 0 100%; - -webkit-transition: background-size 0.2s ease; - transition: background-size 0.2s ease; + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(95%, transparent), + color-stop(95%, var(--clr-forms-focused-color, #0072a3)) + ) + no-repeat; + background: linear-gradient( + to bottom, + transparent 95%, + var(--clr-forms-focused-color, #0072a3) 95% + ) + no-repeat; + background-size: 0 100%; + -webkit-transition: background-size 0.2s ease; + transition: background-size 0.2s ease; } .clr-input:not([readonly]).clr-focus, .clr-input:not([readonly]):focus { - border-bottom-color: #0072a3; - border-bottom-color: var(--clr-forms-focused-color, #0072a3); - background-size: 100% 100%; + border-bottom-color: #0072a3; + border-bottom-color: var(--clr-forms-focused-color, #0072a3); + background-size: 100% 100%; } .clr-input:disabled { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-error .clr-input:not([readonly]) { - border-bottom-color: #c21d00; - border-bottom-color: var(--clr-forms-invalid-color, #c21d00); - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(95%, transparent), - color-stop(95%, var(--clr-forms-invalid-color, #c21d00)) - ) - no-repeat; - background: linear-gradient( - to bottom, - transparent 95%, - var(--clr-forms-invalid-color, #c21d00) 95% - ) - no-repeat; - background-size: 0 100%; - -webkit-transition: background-size 0.2s ease; - transition: background-size 0.2s ease; + border-bottom-color: #c21d00; + border-bottom-color: var(--clr-forms-invalid-color, #c21d00); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(95%, transparent), + color-stop(95%, var(--clr-forms-invalid-color, #c21d00)) + ) + no-repeat; + background: linear-gradient( + to bottom, + transparent 95%, + var(--clr-forms-invalid-color, #c21d00) 95% + ) + no-repeat; + background-size: 0 100%; + -webkit-transition: background-size 0.2s ease; + transition: background-size 0.2s ease; } .clr-error .clr-input:not([readonly]).clr-focus, .clr-error .clr-input:not([readonly]):focus { - border-bottom-color: #c21d00; - border-bottom-color: var(--clr-forms-invalid-color, #c21d00); - background-size: 100% 100%; + border-bottom-color: #c21d00; + border-bottom-color: var(--clr-forms-invalid-color, #c21d00); + background-size: 100% 100%; } .clr-form-control.row .clr-input-wrapper { - max-width: calc(100% - 1.2rem); + max-width: calc(100% - 1.2rem); } .clr-form-compact .clr-input { - max-width: 100%; + max-width: 100%; } .clr-form-compact .clr-input ~ .clr-subtext { - margin-left: 0.3rem; + margin-left: 0.3rem; } .clr-form-control-readonly .clr-input { - border: none; + border: none; } .clr-form-control-multi .clr-input { - max-width: calc(100% + 1.2rem); + max-width: calc(100% + 1.2rem); } ::-webkit-input-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } :-ms-input-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } ::-ms-input-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } ::-webkit-input-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } ::-moz-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } :-ms-input-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } ::-ms-input-placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } ::placeholder { - color: #8c8c8c; - color: var(--clr-forms-placeholder-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-placeholder-color, #8c8c8c); } .clr-input-group { - color: #000; - color: var(--clr-forms-text-color, #000); - border-bottom: 0.05rem solid; - border-bottom-color: #b3b3b3; - border-bottom-color: var(--clr-forms-border-color, #b3b3b3); - display: inline-block; - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(95%, transparent), - color-stop(95%, var(--clr-forms-focused-color, #0072a3)) - ) - no-repeat; - background: linear-gradient( - to bottom, - transparent 95%, - var(--clr-forms-focused-color, #0072a3) 95% - ) - no-repeat; - background-size: 0 100%; - -webkit-transition: background-size 0.2s ease; - transition: background-size 0.2s ease; - max-width: 100%; - height: 1.2rem; + color: #000; + color: var(--clr-forms-text-color, #000); + border-bottom: 0.05rem solid; + border-bottom-color: #b3b3b3; + border-bottom-color: var(--clr-forms-border-color, #b3b3b3); + display: inline-block; + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(95%, transparent), + color-stop(95%, var(--clr-forms-focused-color, #0072a3)) + ) + no-repeat; + background: linear-gradient( + to bottom, + transparent 95%, + var(--clr-forms-focused-color, #0072a3) 95% + ) + no-repeat; + background-size: 0 100%; + -webkit-transition: background-size 0.2s ease; + transition: background-size 0.2s ease; + max-width: 100%; + height: 1.2rem; } .clr-input-group.clr-focus, .clr-input-group:focus { - border-bottom-color: #0072a3; - border-bottom-color: var(--clr-forms-focused-color, #0072a3); - background-size: 100% 100%; + border-bottom-color: #0072a3; + border-bottom-color: var(--clr-forms-focused-color, #0072a3); + background-size: 100% 100%; } .clr-input-group .clr-input { - background: 0 0; - border: none; - margin-right: 0; - max-width: 100%; + background: 0 0; + border: none; + margin-right: 0; + max-width: 100%; } .clr-input-group .clr-input:not([readonly]):focus { - background-size: 0; - border: 0; + background-size: 0; + border: 0; } .clr-input-group select { - border: none; + border: none; } .clr-input-group .clr-multiselect-wrapper select, .clr-input-group .clr-select-wrapper select { - border: none; + border: none; } .clr-input-group .clr-input-group-addon { - color: #8c8c8c; - color: var(--clr-forms-subtext-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-subtext-color, #8c8c8c); } .clr-input-group .clr-input-group-addon:first-child { - padding: 0 0 0 0.45rem; + padding: 0 0 0 0.45rem; } .clr-input-group .clr-input-group-addon:last-child { - padding: 0 0.45rem 0 0; + padding: 0 0.45rem 0 0; } .clr-input-group .clr-input-group-icon-action { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - color: #0072a3; - color: var(--clr-forms-focused-color, #0072a3); - padding: 0 0.45rem; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + color: #0072a3; + color: var(--clr-forms-focused-color, #0072a3); + padding: 0 0.45rem; } button.clr-input-group .clr-input-group-icon-action { - cursor: pointer; + cursor: pointer; } .clr-input-group .clr-input-group-icon-action cds-icon { - height: 0.9rem; - width: 0.9rem; - -webkit-transform: translate(-0.05rem, -0.05rem); - transform: translate(-0.05rem, -0.05rem); + height: 0.9rem; + width: 0.9rem; + -webkit-transform: translate(-0.05rem, -0.05rem); + transform: translate(-0.05rem, -0.05rem); } .clr-form-control-disabled .clr-input-group-icon-action { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-error .clr-input-group { - border-bottom-color: #c21d00; - border-bottom-color: var(--clr-forms-invalid-color, #c21d00); - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(95%, transparent), - color-stop(95%, var(--clr-forms-invalid-color, #c21d00)) - ) - no-repeat; - background: linear-gradient( - to bottom, - transparent 95%, - var(--clr-forms-invalid-color, #c21d00) 95% - ) - no-repeat; - background-size: 0 100%; - -webkit-transition: background-size 0.2s ease; - transition: background-size 0.2s ease; + border-bottom-color: #c21d00; + border-bottom-color: var(--clr-forms-invalid-color, #c21d00); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(95%, transparent), + color-stop(95%, var(--clr-forms-invalid-color, #c21d00)) + ) + no-repeat; + background: linear-gradient( + to bottom, + transparent 95%, + var(--clr-forms-invalid-color, #c21d00) 95% + ) + no-repeat; + background-size: 0 100%; + -webkit-transition: background-size 0.2s ease; + transition: background-size 0.2s ease; } .clr-error .clr-input-group.clr-focus, .clr-error .clr-input-group:focus { - border-bottom-color: #c21d00; - border-bottom-color: var(--clr-forms-invalid-color, #c21d00); - background-size: 100% 100%; + border-bottom-color: #c21d00; + border-bottom-color: var(--clr-forms-invalid-color, #c21d00); + background-size: 100% 100%; } .clr-form-horizontal .clr-input-group { - max-width: 100%; - padding-right: 0; + max-width: 100%; + padding-right: 0; } .clr-radio-wrapper { - position: relative; - line-height: 1.2rem; + position: relative; + line-height: 1.2rem; } .clr-radio-wrapper .clr-control-label { - font-weight: 400; - display: block; + font-weight: 400; + display: block; } .clr-radio-wrapper ~ .clr-validate-icon { - margin-left: 0; + margin-left: 0; } .clr-radio-wrapper ~ .clr-subtext { - display: inline-block; + display: inline-block; } -.clr-radio-wrapper input[type='radio'] { - position: absolute; - opacity: 0; - top: 0.25rem; - left: 0; - height: 0.8rem; - width: 0.8rem; +.clr-radio-wrapper input[type="radio"] { + position: absolute; + opacity: 0; + top: 0.25rem; + left: 0; + height: 0.8rem; + width: 0.8rem; } .clr-radio-wrapper label { - position: relative; - display: inline-block; - min-height: 1.2rem; - padding-left: 1.1rem; - margin-top: 0; - cursor: pointer; - line-height: 1.2rem; - color: #454545; - color: var(--clr-forms-radio-label-color, #454545); + position: relative; + display: inline-block; + min-height: 1.2rem; + padding-left: 1.1rem; + margin-top: 0; + cursor: pointer; + line-height: 1.2rem; + color: #454545; + color: var(--clr-forms-radio-label-color, #454545); } .clr-radio-wrapper label:empty { - padding-left: 0; -} -.clr-radio-wrapper input[type='radio'] + label::before { - position: absolute; - top: 0.2rem; - left: 0; - content: ''; - display: inline-block; - height: 0.8rem; - width: 0.8rem; - border: 0.05rem solid; - border-color: #b3b3b3; - border-color: var(--clr-forms-border-color, #b3b3b3); - border-radius: 50%; -} -.clr-radio-wrapper input[type='radio']:checked + label::before { - -webkit-box-shadow: inset 0 0 0 0.3rem #0072a3; - box-shadow: inset 0 0 0 0.3rem #0072a3; - -webkit-box-shadow: var( - --clr-forms-radio-selected-shadow, - inset 0 0 0 0.3rem #0072a3 - ); - box-shadow: var( - --clr-forms-radio-selected-shadow, - inset 0 0 0 0.3rem #0072a3 - ); - border: none; -} -.clr-radio-wrapper input[type='radio']:focus + label::before { - outline: 0; - -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - -webkit-box-shadow: var( - --clr-forms-radio-focused-shadow, - 0 0 0.1rem 0.1rem #69c0e2 - ); - box-shadow: var( - --clr-forms-radio-focused-shadow, - 0 0 0.1rem 0.1rem #69c0e2 - ); -} -.clr-radio-wrapper input[type='radio']:focus:checked + label::before { - outline: 0; - -webkit-box-shadow: - inset 0 0 0 0.3rem #0072a3, - 0 0 0.1rem 0.1rem #69c0e2; - box-shadow: - inset 0 0 0 0.3rem #0072a3, - 0 0 0.1rem 0.1rem #69c0e2; - -webkit-box-shadow: var( - --clr-forms-radio-selected-shadow, - inset 0 0 0 0.3rem #0072a3 - ), - var(--clr-forms-radio-focused-shadow, 0 0 0.1rem 0.1rem #69c0e2); - box-shadow: var( - --clr-forms-radio-selected-shadow, - inset 0 0 0 0.3rem #0072a3 - ), - var(--clr-forms-radio-focused-shadow, 0 0 0.1rem 0.1rem #69c0e2); -} -.clr-radio-wrapper.disabled input[type='radio']:checked + label::before { - background-color: #ccc; - background-color: var(--clr-forms-radio-disabled-background-color, #ccc); - -webkit-box-shadow: inset 0 0 0 0.3rem #0072a3; - box-shadow: inset 0 0 0 0.3rem #0072a3; - -webkit-box-shadow: var( - --clr-forms-radio-disabled-shadow, - inset 0 0 0 0.3rem #0072a3 - ); - box-shadow: var( - --clr-forms-radio-disabled-shadow, - inset 0 0 0 0.3rem #0072a3 - ); + padding-left: 0; +} +.clr-radio-wrapper input[type="radio"] + label::before { + position: absolute; + top: 0.2rem; + left: 0; + content: ""; + display: inline-block; + height: 0.8rem; + width: 0.8rem; + border: 0.05rem solid; + border-color: #b3b3b3; + border-color: var(--clr-forms-border-color, #b3b3b3); + border-radius: 50%; +} +.clr-radio-wrapper input[type="radio"]:checked + label::before { + -webkit-box-shadow: inset 0 0 0 0.3rem #0072a3; + box-shadow: inset 0 0 0 0.3rem #0072a3; + -webkit-box-shadow: var( + --clr-forms-radio-selected-shadow, + inset 0 0 0 0.3rem #0072a3 + ); + box-shadow: var( + --clr-forms-radio-selected-shadow, + inset 0 0 0 0.3rem #0072a3 + ); + border: none; +} +.clr-radio-wrapper input[type="radio"]:focus + label::before { + outline: 0; + -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + -webkit-box-shadow: var( + --clr-forms-radio-focused-shadow, + 0 0 0.1rem 0.1rem #69c0e2 + ); + box-shadow: var(--clr-forms-radio-focused-shadow, 0 0 0.1rem 0.1rem #69c0e2); +} +.clr-radio-wrapper input[type="radio"]:focus:checked + label::before { + outline: 0; + -webkit-box-shadow: + inset 0 0 0 0.3rem #0072a3, + 0 0 0.1rem 0.1rem #69c0e2; + box-shadow: + inset 0 0 0 0.3rem #0072a3, + 0 0 0.1rem 0.1rem #69c0e2; + -webkit-box-shadow: + var(--clr-forms-radio-selected-shadow, inset 0 0 0 0.3rem #0072a3), + var(--clr-forms-radio-focused-shadow, 0 0 0.1rem 0.1rem #69c0e2); + box-shadow: + var(--clr-forms-radio-selected-shadow, inset 0 0 0 0.3rem #0072a3), + var(--clr-forms-radio-focused-shadow, 0 0 0.1rem 0.1rem #69c0e2); +} +.clr-radio-wrapper.disabled input[type="radio"]:checked + label::before { + background-color: #ccc; + background-color: var(--clr-forms-radio-disabled-background-color, #ccc); + -webkit-box-shadow: inset 0 0 0 0.3rem #0072a3; + box-shadow: inset 0 0 0 0.3rem #0072a3; + -webkit-box-shadow: var( + --clr-forms-radio-disabled-shadow, + inset 0 0 0 0.3rem #0072a3 + ); + box-shadow: var( + --clr-forms-radio-disabled-shadow, + inset 0 0 0 0.3rem #0072a3 + ); } .clr-form-control-disabled .clr-radio-wrapper label { - cursor: not-allowed; + cursor: not-allowed; } -.clr-error .clr-radio-wrapper input[type='radio'] + label::before { - border-color: #c21d00; - border-color: var(--clr-forms-invalid-color, #c21d00); +.clr-error .clr-radio-wrapper input[type="radio"] + label::before { + border-color: #c21d00; + border-color: var(--clr-forms-invalid-color, #c21d00); } .clr-form-compact .clr-error .clr-subtext { - margin-left: 0; + margin-left: 0; } .clr-form-compact .clr-radio-wrapper { - max-width: 100%; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -ms-flex-line-pack: start; - align-content: flex-start; - height: 1.2rem; + max-width: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -ms-flex-line-pack: start; + align-content: flex-start; + height: 1.2rem; } .clr-form-compact .clr-radio-wrapper ~ .clr-subtext { - margin-left: 0.3rem; + margin-left: 0.3rem; } .clr-form-compact .clr-radio-wrapper .clr-control-label { - display: inline-block; - margin-right: 0.6rem; - width: auto; + display: inline-block; + margin-right: 0.6rem; + width: auto; } .clr-form-compact .clr-radio-wrapper .clr-control-label { - margin-top: 0; + margin-top: 0; } .clr-multiselect-wrapper, .clr-select-wrapper { - position: relative; - white-space: nowrap; + position: relative; + white-space: nowrap; } .clr-multiselect-wrapper select, .clr-select-wrapper select { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - height: 1.2rem; - color: #000; - color: var(--clr-forms-text-color, #000); - border-bottom: 0.05rem solid; - border-bottom-color: #b3b3b3; - border-bottom-color: var(--clr-forms-border-color, #b3b3b3); - display: inline-block; - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(95%, transparent), - color-stop(95%, var(--clr-forms-focused-color, #0072a3)) - ) - no-repeat; - background: linear-gradient( - to bottom, - transparent 95%, - var(--clr-forms-focused-color, #0072a3) 95% - ) - no-repeat; - background-size: 0 100%; - -webkit-transition: background-size 0.2s ease; - transition: background-size 0.2s ease; - position: relative; - padding: 0 1.1rem 0 0.3rem; - cursor: pointer; - font-size: 0.65rem; - z-index: 2; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + height: 1.2rem; + color: #000; + color: var(--clr-forms-text-color, #000); + border-bottom: 0.05rem solid; + border-bottom-color: #b3b3b3; + border-bottom-color: var(--clr-forms-border-color, #b3b3b3); + display: inline-block; + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(95%, transparent), + color-stop(95%, var(--clr-forms-focused-color, #0072a3)) + ) + no-repeat; + background: linear-gradient( + to bottom, + transparent 95%, + var(--clr-forms-focused-color, #0072a3) 95% + ) + no-repeat; + background-size: 0 100%; + -webkit-transition: background-size 0.2s ease; + transition: background-size 0.2s ease; + position: relative; + padding: 0 1.1rem 0 0.3rem; + cursor: pointer; + font-size: 0.65rem; + z-index: 2; } .clr-multiselect-wrapper select:focus, .clr-select-wrapper select:focus { - outline: 0; + outline: 0; } .clr-multiselect-wrapper select.clr-focus, .clr-multiselect-wrapper select:focus, .clr-select-wrapper select.clr-focus, .clr-select-wrapper select:focus { - border-bottom-color: #0072a3; - border-bottom-color: var(--clr-forms-focused-color, #0072a3); - background-size: 100% 100%; + border-bottom-color: #0072a3; + border-bottom-color: var(--clr-forms-focused-color, #0072a3); + background-size: 100% 100%; } .clr-multiselect-wrapper select:active, .clr-multiselect-wrapper select:hover, .clr-select-wrapper select:active, .clr-select-wrapper select:hover { - border-color: rgba(222, 222, 222, 0.5); - border-color: var( - --clr-forms-select-hover-background, - rgba(222, 222, 222, 0.5) - ); - background: rgba(222, 222, 222, 0.5); - background: var( - --clr-forms-select-hover-background, - rgba(222, 222, 222, 0.5) - ); + border-color: rgba(222, 222, 222, 0.5); + border-color: var( + --clr-forms-select-hover-background, + rgba(222, 222, 222, 0.5) + ); + background: rgba(222, 222, 222, 0.5); + background: var( + --clr-forms-select-hover-background, + rgba(222, 222, 222, 0.5) + ); } .clr-multiselect-wrapper select:disabled, .clr-select-wrapper select:disabled { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-multiselect-wrapper select option, .clr-select-wrapper select option { - color: #000; - color: var(--clr-forms-select-option-color, #000); + color: #000; + color: var(--clr-forms-select-option-color, #000); } .clr-multiselect-wrapper select::-ms-expand, .clr-select-wrapper select::-ms-expand { - display: none; + display: none; } .clr-select-wrapper { - max-height: 1.2rem; - display: inline-block; + max-height: 1.2rem; + display: inline-block; } .clr-select-wrapper::after { - position: absolute; - content: ''; - height: 0.5rem; - width: 0.5rem; - top: 0.35rem; - right: 0.3rem; - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%23b3b3b3%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A'); - background-repeat: no-repeat; - background-size: contain; - vertical-align: middle; - margin: 0; + position: absolute; + content: ""; + height: 0.5rem; + width: 0.5rem; + top: 0.35rem; + right: 0.3rem; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%23b3b3b3%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A"); + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; + margin: 0; } .clr-select-wrapper:hover::after { - color: #8c8c8c; - color: var(--clr-forms-select-caret-hover-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-forms-select-caret-hover-color, #8c8c8c); } .clr-error .clr-select-wrapper::after { - right: 1.5rem; + right: 1.5rem; } .clr-multiselect-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .clr-multiselect-wrapper .clr-validate-icon { - margin-left: 0; + margin-left: 0; } select[multiple], select[size] { - padding: 0; - background: #fff; - background: var(--clr-forms-select-multiple-background-color, #fff); - border: 0.05rem solid; - border-color: #ccc; - border-color: var(--clr-forms-select-multiple-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - height: auto; - min-width: 6rem; + padding: 0; + background: #fff; + background: var(--clr-forms-select-multiple-background-color, #fff); + border: 0.05rem solid; + border-color: #ccc; + border-color: var(--clr-forms-select-multiple-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + height: auto; + min-width: 6rem; } select[multiple]:active, select[multiple]:hover, select[size]:active, select[size]:hover { - background: #fff; - background: var(--clr-forms-select-multiple-background-color, #fff); - border-color: #ccc; - border-color: var(--clr-forms-select-multiple-border-color, #ccc); + background: #fff; + background: var(--clr-forms-select-multiple-background-color, #fff); + border-color: #ccc; + border-color: var(--clr-forms-select-multiple-border-color, #ccc); } select[multiple] option, select[size] option { - padding: 0.15rem 0.3rem; - color: #000; - color: var(--clr-forms-select-multiple-option-color, #000); + padding: 0.15rem 0.3rem; + color: #000; + color: var(--clr-forms-select-multiple-option-color, #000); } .clr-error select:not([multiple]) { - border-bottom-color: #c21d00; - border-bottom-color: var(--clr-forms-invalid-color, #c21d00); + border-bottom-color: #c21d00; + border-bottom-color: var(--clr-forms-invalid-color, #c21d00); } .clr-error select[multiple] { - border-color: #c21d00; - border-color: var(--clr-forms-invalid-color, #c21d00); + border-color: #c21d00; + border-color: var(--clr-forms-invalid-color, #c21d00); } .clr-error select[multiple]:focus { - outline: 0; - -webkit-box-shadow: 0 0.05rem 0.15rem #ff745c; - box-shadow: 0 0.05rem 0.15rem #ff745c; - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-forms-select-multiple-error-focus-color, #ff745c); - box-shadow: 0 0.05rem 0.15rem - var(--clr-forms-select-multiple-error-focus-color, #ff745c); + outline: 0; + -webkit-box-shadow: 0 0.05rem 0.15rem #ff745c; + box-shadow: 0 0.05rem 0.15rem #ff745c; + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-forms-select-multiple-error-focus-color, #ff745c); + box-shadow: 0 0.05rem 0.15rem + var(--clr-forms-select-multiple-error-focus-color, #ff745c); } .clr-form-control-disabled .clr-select.disabled { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-form-control-disabled .clr-select.disabled:hover::after { - color: #b3b3b3; - color: var(--clr-forms-select-caret-color, #b3b3b3); + color: #b3b3b3; + color: var(--clr-forms-select-caret-color, #b3b3b3); } .clr-form-control-disabled .clr-select select:disabled, .clr-form-control-disabled .clr-select.disabled > select { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-form-control-disabled .clr-select select:disabled:hover, .clr-form-control-disabled .clr-select.disabled > select:hover { - background: 0 0; - border-color: #b3b3b3; - border-color: var(--clr-forms-border-color, #b3b3b3); + background: 0 0; + border-color: #b3b3b3; + border-color: var(--clr-forms-border-color, #b3b3b3); } .clr-form-compact .clr-multiselect-wrapper { - margin-top: 0; + margin-top: 0; } .clr-textarea-wrapper { - white-space: nowrap; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - margin-top: 0.3rem; + white-space: nowrap; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + margin-top: 0.3rem; } .clr-textarea-wrapper .clr-validate-icon { - margin-left: 0; + margin-left: 0; } .clr-textarea { - max-width: calc(100% - 0.3rem); - height: auto; - resize: vertical; - background: #fff; - background: var(--clr-forms-textarea-background-color, #fff); - border: 0.05rem solid; - border-color: #b3b3b3; - border-color: var(--clr-forms-border-color, #b3b3b3); - color: #000; - color: var(--clr-forms-text-color, #000); - border-radius: 0.15rem; - padding: 0.4rem 0.6rem; - font-size: 0.65rem; + max-width: calc(100% - 0.3rem); + height: auto; + resize: vertical; + background: #fff; + background: var(--clr-forms-textarea-background-color, #fff); + border: 0.05rem solid; + border-color: #b3b3b3; + border-color: var(--clr-forms-border-color, #b3b3b3); + color: #000; + color: var(--clr-forms-text-color, #000); + border-radius: 0.15rem; + padding: 0.4rem 0.6rem; + font-size: 0.65rem; } .clr-textarea:focus { - outline: 0; - -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - -webkit-box-shadow: var( - --clr-forms-textarea-focused-outline, - 0 0 0.1rem 0.1rem #69c0e2 - ); - box-shadow: var( - --clr-forms-textarea-focused-outline, - 0 0 0.1rem 0.1rem #69c0e2 - ); + outline: 0; + -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + -webkit-box-shadow: var( + --clr-forms-textarea-focused-outline, + 0 0 0.1rem 0.1rem #69c0e2 + ); + box-shadow: var( + --clr-forms-textarea-focused-outline, + 0 0 0.1rem 0.1rem #69c0e2 + ); } .clr-textarea:disabled { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-error .clr-textarea { - border-color: #c21d00; - border-color: var(--clr-forms-invalid-color, #c21d00); + border-color: #c21d00; + border-color: var(--clr-forms-invalid-color, #c21d00); } .clr-error .clr-textarea:focus { - outline: 0; - -webkit-box-shadow: 0 0 0.1rem 0.1rem #ff745c; - box-shadow: 0 0 0.1rem 0.1rem #ff745c; - -webkit-box-shadow: var( - --clr-forms-textarea-invalid-focused-outline, - 0 0 0.1rem 0.1rem #ff745c - ); - box-shadow: var( - --clr-forms-textarea-invalid-focused-outline, - 0 0 0.1rem 0.1rem #ff745c - ); + outline: 0; + -webkit-box-shadow: 0 0 0.1rem 0.1rem #ff745c; + box-shadow: 0 0 0.1rem 0.1rem #ff745c; + -webkit-box-shadow: var( + --clr-forms-textarea-invalid-focused-outline, + 0 0 0.1rem 0.1rem #ff745c + ); + box-shadow: var( + --clr-forms-textarea-invalid-focused-outline, + 0 0 0.1rem 0.1rem #ff745c + ); } .clr-control-container textarea { - width: auto; + width: auto; } .clr-form-control.row .clr-textarea-wrapper { - max-width: calc(100% - 0.3rem); + max-width: calc(100% - 0.3rem); } .clr-form-compact .clr-textarea-wrapper { - margin-top: 0; + margin-top: 0; } .clr-form-compact .clr-textarea-wrapper .clr-textarea { - max-width: 100%; + max-width: 100%; } .clr-form-compact .clr-textarea-wrapper .clr-textarea ~ .clr-subtext { - margin-left: 0.3rem; + margin-left: 0.3rem; } :root { - --clr-toggle-bg-color-off: var(--clr-color-neutral-600); - --clr-toggle-bg-color-on: var(--clr-color-success-500); - --clr-toggle-handle-bg-color: var(--clr-color-neutral-50); - --clr-toggle-disabled-default-border-color: var(--clr-color-neutral-400); - --clr-toggle-disabled-default-handle-color: var(--clr-color-neutral-0); - --clr-toggle-disabled-off-border-color: var( - --clr-toggle-disabled-default-border-color - ); - --clr-toggle-disabled-off-bg-color: var( - --clr-toggle-disabled-default-handle-color - ); - --clr-toggle-disabled-off-handle-border-color: var( - --clr-toggle-disabled-default-border-color - ); - --clr-toggle-disabled-on-border-color: var( - --clr-toggle-disabled-default-border-color - ); - --clr-toggle-disabled-on-bg-color: var( - --clr-toggle-disabled-default-border-color - ); - --clr-toggle-disabled-on-handle-border-color: var( - --clr-toggle-disabled-default-handle-color - ); -} -.clr-toggle-wrapper input[type='checkbox'] { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; + --clr-toggle-bg-color-off: var(--clr-color-neutral-600); + --clr-toggle-bg-color-on: var(--clr-color-success-500); + --clr-toggle-handle-bg-color: var(--clr-color-neutral-50); + --clr-toggle-disabled-default-border-color: var(--clr-color-neutral-400); + --clr-toggle-disabled-default-handle-color: var(--clr-color-neutral-0); + --clr-toggle-disabled-off-border-color: var( + --clr-toggle-disabled-default-border-color + ); + --clr-toggle-disabled-off-bg-color: var( + --clr-toggle-disabled-default-handle-color + ); + --clr-toggle-disabled-off-handle-border-color: var( + --clr-toggle-disabled-default-border-color + ); + --clr-toggle-disabled-on-border-color: var( + --clr-toggle-disabled-default-border-color + ); + --clr-toggle-disabled-on-bg-color: var( + --clr-toggle-disabled-default-border-color + ); + --clr-toggle-disabled-on-handle-border-color: var( + --clr-toggle-disabled-default-handle-color + ); +} +.clr-toggle-wrapper input[type="checkbox"] { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; } .clr-toggle-wrapper { - height: 1.2rem; - vertical-align: middle; - position: relative; - display: block; - margin-right: 0.6rem; + height: 1.2rem; + vertical-align: middle; + position: relative; + display: block; + margin-right: 0.6rem; } .clr-toggle-wrapper label { - display: inline-block; - position: relative; - cursor: pointer; - height: 1.2rem; - margin-left: 2.1rem; - margin-right: 0; -} -.clr-toggle-wrapper input[type='checkbox'] { - position: absolute; - top: 0.3rem; - right: 0.3rem; - height: 0.8rem; - width: 0.8rem; - opacity: 0; -} -.clr-toggle-wrapper input[type='checkbox'] + label::before { - position: absolute; - display: inline-block; - content: ''; - height: 0.9rem; - width: 1.65rem; - border: 0.1rem solid; - border-radius: 0.45rem; - border-color: #8c8c8c; - border-color: var(--clr-toggle-bg-color-off, #8c8c8c); - background-color: #8c8c8c; - background-color: var(--clr-toggle-bg-color-off, #8c8c8c); - top: 0.15rem; - right: 0; - left: -2.1rem; - -webkit-transition-duration: 0.15s; - transition-duration: 0.15s; - -webkit-transition-timing-function: ease-in; - transition-timing-function: ease-in; - -webkit-transition-property: border-color, background-color; - transition-property: border-color, background-color; -} -.clr-toggle-wrapper input[type='checkbox']:focus + label::before { - outline: 0; - -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; - box-shadow: 0 0 0.1rem 0.1rem #69c0e2; -} -.clr-toggle-wrapper input[type='checkbox']:checked + label::before { - border-color: #5aa220; - border-color: var(--clr-toggle-bg-color-on, #5aa220); - background-color: #5aa220; - background-color: var(--clr-toggle-bg-color-on, #5aa220); - -webkit-transition-duration: 0.15s; - transition-duration: 0.15s; - -webkit-transition-timing-function: ease-in; - transition-timing-function: ease-in; - -webkit-transition-property: border-color, background-color; - transition-property: border-color, background-color; -} -.clr-toggle-wrapper input[type='checkbox'] + label::after { - position: absolute; - display: inline-block; - content: ''; - height: 0.7rem; - width: 0.7rem; - border-width: 0.05rem; - border-color: #fafafa; - border-color: var(--clr-toggle-handle-bg-color, #fafafa); - border-style: solid; - border-radius: 50%; - background-color: #fafafa; - background-color: var(--clr-toggle-handle-bg-color, #fafafa); - top: 0.25rem; - right: 0; - left: -2rem; - -webkit-transition-duration: 0.15s; - transition-duration: 0.15s; - -webkit-transition-timing-function: ease-in; - transition-timing-function: ease-in; - -webkit-transition-property: right, left; - transition-property: right, left; -} -.clr-toggle-wrapper input[type='checkbox']:checked + label::after { - right: -2rem; - left: -1.25rem; - -webkit-transition-duration: 0.15s; - transition-duration: 0.15s; - -webkit-transition-timing-function: ease-in; - transition-timing-function: ease-in; - -webkit-transition-property: right, left; - transition-property: right, left; + display: inline-block; + position: relative; + cursor: pointer; + height: 1.2rem; + margin-left: 2.1rem; + margin-right: 0; +} +.clr-toggle-wrapper input[type="checkbox"] { + position: absolute; + top: 0.3rem; + right: 0.3rem; + height: 0.8rem; + width: 0.8rem; + opacity: 0; +} +.clr-toggle-wrapper input[type="checkbox"] + label::before { + position: absolute; + display: inline-block; + content: ""; + height: 0.9rem; + width: 1.65rem; + border: 0.1rem solid; + border-radius: 0.45rem; + border-color: #8c8c8c; + border-color: var(--clr-toggle-bg-color-off, #8c8c8c); + background-color: #8c8c8c; + background-color: var(--clr-toggle-bg-color-off, #8c8c8c); + top: 0.15rem; + right: 0; + left: -2.1rem; + -webkit-transition-duration: 0.15s; + transition-duration: 0.15s; + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + -webkit-transition-property: border-color, background-color; + transition-property: border-color, background-color; +} +.clr-toggle-wrapper input[type="checkbox"]:focus + label::before { + outline: 0; + -webkit-box-shadow: 0 0 0.1rem 0.1rem #69c0e2; + box-shadow: 0 0 0.1rem 0.1rem #69c0e2; +} +.clr-toggle-wrapper input[type="checkbox"]:checked + label::before { + border-color: #5aa220; + border-color: var(--clr-toggle-bg-color-on, #5aa220); + background-color: #5aa220; + background-color: var(--clr-toggle-bg-color-on, #5aa220); + -webkit-transition-duration: 0.15s; + transition-duration: 0.15s; + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + -webkit-transition-property: border-color, background-color; + transition-property: border-color, background-color; +} +.clr-toggle-wrapper input[type="checkbox"] + label::after { + position: absolute; + display: inline-block; + content: ""; + height: 0.7rem; + width: 0.7rem; + border-width: 0.05rem; + border-color: #fafafa; + border-color: var(--clr-toggle-handle-bg-color, #fafafa); + border-style: solid; + border-radius: 50%; + background-color: #fafafa; + background-color: var(--clr-toggle-handle-bg-color, #fafafa); + top: 0.25rem; + right: 0; + left: -2rem; + -webkit-transition-duration: 0.15s; + transition-duration: 0.15s; + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + -webkit-transition-property: right, left; + transition-property: right, left; +} +.clr-toggle-wrapper input[type="checkbox"]:checked + label::after { + right: -2rem; + left: -1.25rem; + -webkit-transition-duration: 0.15s; + transition-duration: 0.15s; + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + -webkit-transition-property: right, left; + transition-property: right, left; } .clr-toggle-wrapper.disabled label { - opacity: 0.4; - cursor: not-allowed; -} -.clr-toggle-wrapper.disabled input[type='checkbox']:checked + label::before { - border-color: #ccc; - border-color: var(--clr-toggle-disabled-on-border-color, #ccc); - background-color: #ccc; - background-color: var(--clr-toggle-disabled-on-border-color, #ccc); -} -.clr-toggle-wrapper input[type='checkbox']:disabled + label { - cursor: not-allowed; -} -.clr-toggle-wrapper input[type='checkbox']:disabled + label::before { - background-color: #fff; - background-color: var(--clr-toggle-disabled-off-bg-color, #fff); - border-color: #ccc; - border-color: var(--clr-toggle-disabled-off-border-color, #ccc); -} -.clr-toggle-wrapper input[type='checkbox']:disabled + label::after { - background-color: #fff; - background-color: var(--clr-toggle-disabled-off-bg-color, #fff); - border-width: 0.1rem; - border-style: solid; - border-color: #ccc; - border-color: var(--clr-toggle-disabled-off-handle-border-color, #ccc); - height: 0.9rem; - width: 0.9rem; - top: 0.15rem; -} -.clr-toggle-wrapper input[type='checkbox']:checked:disabled + label::before { - border-color: #ccc; - border-color: var(--clr-toggle-disabled-on-border-color, #ccc); - background-color: #ccc; - background-color: var(--clr-toggle-disabled-on-bg-color, #ccc); -} -.clr-toggle-wrapper input[type='checkbox']:checked:disabled + label::after { - border-color: #fff; - border-color: var(--clr-toggle-disabled-on-handle-border-color, #fff); - height: 0.7rem; - width: 0.7rem; - top: 0.25rem; - left: -1.25rem; -} -.clr-toggle-wrapper input[type='checkbox']:disabled + label::after { - left: -2.1rem; -} -.clr-toggle-wrapper.right-label label { - margin-left: 2.1rem; - margin-right: 0; -} -.clr-toggle-wrapper.right-label input[type='checkbox'] + label::before { - right: 0; - left: -2.1rem; -} -.clr-toggle-wrapper.right-label input[type='checkbox'] + label::after { - right: 0; - left: -2rem; - -webkit-transition-property: left; - transition-property: left; -} -.clr-toggle-wrapper.right-label input[type='checkbox']:checked + label::after { - left: -1.25rem; - -webkit-transition-property: left; - transition-property: left; -} -.clr-toggle-wrapper .clr-control-label { - display: block; - font-weight: 400; - line-height: 1.2rem; -} -.clr-control-inline .clr-toggle-wrapper .clr-control-label { - display: inline-block; - margin-right: 0.6rem; -} -.clr-toggle-right label { - display: inline-block; - margin-right: 2.1rem; - margin-left: 0; -} -.clr-toggle-right input[type='checkbox'] + label::before { - left: unset; - right: -2.1rem; -} -.clr-toggle-right input[type='checkbox'] + label::after { - left: unset; - right: -1.25rem; - -webkit-transition-property: right; - transition-property: right; -} -.clr-toggle-right input[type='checkbox']:checked + label::after { - left: unset; - right: -2rem; - -webkit-transition-property: right; - transition-property: right; -} -.clr-toggle-right input[type='checkbox']:disabled + label::after { - left: unset; - right: -1.3rem; -} -.clr-toggle-right input[type='checkbox']:checked:disabled + label::after { - left: unset; - right: -2rem; -} -.clr-toggle-right .clr-control-inline .clr-toggle-wrapper .clr-control-label { - margin-right: 2.7rem; -} -.clr-range-wrapper { - position: relative; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; - height: 0.7rem; - white-space: nowrap; -} -.clr-range-wrapper .fill-input { - position: absolute; - left: 0; - display: inline-block; - height: 0.2rem; - pointer-events: none; - cursor: pointer; - z-index: 10; - background-color: #0072a3; - background-color: var(--clr-forms-range-progress-fill-color, #0072a3); -} -.clr-range { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; - height: 0.7rem; + opacity: 0.4; + cursor: not-allowed; +} +.clr-toggle-wrapper.disabled input[type="checkbox"]:checked + label::before { + border-color: #ccc; + border-color: var(--clr-toggle-disabled-on-border-color, #ccc); + background-color: #ccc; + background-color: var(--clr-toggle-disabled-on-border-color, #ccc); +} +.clr-toggle-wrapper input[type="checkbox"]:disabled + label { + cursor: not-allowed; +} +.clr-toggle-wrapper input[type="checkbox"]:disabled + label::before { + background-color: #fff; + background-color: var(--clr-toggle-disabled-off-bg-color, #fff); + border-color: #ccc; + border-color: var(--clr-toggle-disabled-off-border-color, #ccc); +} +.clr-toggle-wrapper input[type="checkbox"]:disabled + label::after { + background-color: #fff; + background-color: var(--clr-toggle-disabled-off-bg-color, #fff); + border-width: 0.1rem; + border-style: solid; + border-color: #ccc; + border-color: var(--clr-toggle-disabled-off-handle-border-color, #ccc); + height: 0.9rem; + width: 0.9rem; + top: 0.15rem; +} +.clr-toggle-wrapper input[type="checkbox"]:checked:disabled + label::before { + border-color: #ccc; + border-color: var(--clr-toggle-disabled-on-border-color, #ccc); + background-color: #ccc; + background-color: var(--clr-toggle-disabled-on-bg-color, #ccc); +} +.clr-toggle-wrapper input[type="checkbox"]:checked:disabled + label::after { + border-color: #fff; + border-color: var(--clr-toggle-disabled-on-handle-border-color, #fff); + height: 0.7rem; + width: 0.7rem; + top: 0.25rem; + left: -1.25rem; +} +.clr-toggle-wrapper input[type="checkbox"]:disabled + label::after { + left: -2.1rem; } -.clr-range:disabled, -.clr-range:disabled + .fill-input { - pointer-events: auto; - cursor: not-allowed; +.clr-toggle-wrapper.right-label label { + margin-left: 2.1rem; + margin-right: 0; } -input[type='range'] { - padding: 0; - -webkit-appearance: none; - left: 0; - height: 0.2rem; - background-color: #e8e8e8; - background-color: var(--clr-forms-range-track-color, #e8e8e8); +.clr-toggle-wrapper.right-label input[type="checkbox"] + label::before { + right: 0; + left: -2.1rem; } -input[type='range']::-webkit-slider-runnable-track { - height: 0.2rem; - cursor: pointer; - background-color: #e8e8e8; - background-color: var(--clr-forms-range-track-color, #e8e8e8); +.clr-toggle-wrapper.right-label input[type="checkbox"] + label::after { + right: 0; + left: -2rem; + -webkit-transition-property: left; + transition-property: left; } -input[type='range']::-webkit-slider-thumb { - -webkit-appearance: none; - margin-top: -0.25rem; - border-radius: 50%; - height: 0.7rem; - width: 0.7rem; - background-color: #0072a3; - background-color: var(--clr-forms-range-progress-fill-color, #0072a3); +.clr-toggle-wrapper.right-label input[type="checkbox"]:checked + label::after { + left: -1.25rem; + -webkit-transition-property: left; + transition-property: left; } -input[type='range']::-moz-range-track { - height: 0.2rem; - cursor: pointer; - background-color: #e8e8e8; - background-color: var(--clr-forms-range-track-color, #e8e8e8); +.clr-toggle-wrapper .clr-control-label { + display: block; + font-weight: 400; + line-height: 1.2rem; } -input[type='range']::-moz-range-thumb { - border: 0; - margin-top: -0.25rem; - border-radius: 50%; - height: 0.7rem; - width: 0.7rem; - background-color: #0072a3; - background-color: var(--clr-forms-range-progress-fill-color, #0072a3); +.clr-control-inline .clr-toggle-wrapper .clr-control-label { + display: inline-block; + margin-right: 0.6rem; } -@supports (-ms-ime-align: auto) { - .clr-range-wrapper .fill-input { - display: none; - } - .clr-range-wrapper.progress-fill input[type='range']::-ms-fill-lower { - height: 0.2rem; - background-color: #0072a3; - background-color: var(--clr-forms-range-progress-fill-color, #0072a3); - } - input[type='range'] { - border: 0; - margin: 0; - -webkit-appearance: none; - left: 0; - height: 0.7rem; - } - input[type='range']::-ms-track { - margin: 0; - border: 0; - height: 0.2rem; - cursor: pointer; - background-color: #e8e8e8; - background-color: var(--clr-forms-range-track-color, #e8e8e8); - } - input[type='range']::-ms-thumb { - border: 0; - margin-top: 0; - border-radius: 50%; - height: 0.7rem; - width: 0.7rem; - background-color: #0072a3; - background-color: var(--clr-forms-range-progress-fill-color, #0072a3); - } +.clr-toggle-right label { + display: inline-block; + margin-right: 2.1rem; + margin-left: 0; +} +.clr-toggle-right input[type="checkbox"] + label::before { + left: unset; + right: -2.1rem; +} +.clr-toggle-right input[type="checkbox"] + label::after { + left: unset; + right: -1.25rem; + -webkit-transition-property: right; + transition-property: right; +} +.clr-toggle-right input[type="checkbox"]:checked + label::after { + left: unset; + right: -2rem; + -webkit-transition-property: right; + transition-property: right; +} +.clr-toggle-right input[type="checkbox"]:disabled + label::after { + left: unset; + right: -1.3rem; +} +.clr-toggle-right input[type="checkbox"]:checked:disabled + label::after { + left: unset; + right: -2rem; } -_:-ms-fullscreen .clr-range-wrapper .fill-input, -:root .clr-range-wrapper .fill-input { - display: none; +.clr-toggle-right .clr-control-inline .clr-toggle-wrapper .clr-control-label { + margin-right: 2.7rem; } -_:-ms-fullscreen - .clr-range-wrapper.progress-fill - input[type='range']::-ms-fill-lower, -:root .clr-range-wrapper.progress-fill input[type='range']::-ms-fill-lower { +.clr-range-wrapper { + position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; + height: 0.7rem; + white-space: nowrap; +} +.clr-range-wrapper .fill-input { + position: absolute; + left: 0; + display: inline-block; + height: 0.2rem; + pointer-events: none; + cursor: pointer; + z-index: 10; + background-color: #0072a3; + background-color: var(--clr-forms-range-progress-fill-color, #0072a3); +} +.clr-range { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; + height: 0.7rem; +} +.clr-range:disabled, +.clr-range:disabled + .fill-input { + pointer-events: auto; + cursor: not-allowed; +} +input[type="range"] { + padding: 0; + -webkit-appearance: none; + left: 0; + height: 0.2rem; + background-color: #e8e8e8; + background-color: var(--clr-forms-range-track-color, #e8e8e8); +} +input[type="range"]::-webkit-slider-runnable-track { + height: 0.2rem; + cursor: pointer; + background-color: #e8e8e8; + background-color: var(--clr-forms-range-track-color, #e8e8e8); +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + margin-top: -0.25rem; + border-radius: 50%; + height: 0.7rem; + width: 0.7rem; + background-color: #0072a3; + background-color: var(--clr-forms-range-progress-fill-color, #0072a3); +} +input[type="range"]::-moz-range-track { + height: 0.2rem; + cursor: pointer; + background-color: #e8e8e8; + background-color: var(--clr-forms-range-track-color, #e8e8e8); +} +input[type="range"]::-moz-range-thumb { + border: 0; + margin-top: -0.25rem; + border-radius: 50%; + height: 0.7rem; + width: 0.7rem; + background-color: #0072a3; + background-color: var(--clr-forms-range-progress-fill-color, #0072a3); +} +@supports (-ms-ime-align: auto) { + .clr-range-wrapper .fill-input { + display: none; + } + .clr-range-wrapper.progress-fill input[type="range"]::-ms-fill-lower { height: 0.2rem; background-color: #0072a3; background-color: var(--clr-forms-range-progress-fill-color, #0072a3); -} -_:-ms-fullscreen input[type='range']::-ms-tooltip, -:root input[type='range']::-ms-tooltip { - visibility: hidden; -} -_:-ms-fullscreen input[type='range'], -:root input[type='range'] { + } + input[type="range"] { border: 0; margin: 0; -webkit-appearance: none; left: 0; height: 0.7rem; -} -_:-ms-fullscreen input[type='range']::-ms-track, -:root input[type='range']::-ms-track { - border: 0; + } + input[type="range"]::-ms-track { margin: 0; + border: 0; height: 0.2rem; cursor: pointer; background-color: #e8e8e8; background-color: var(--clr-forms-range-track-color, #e8e8e8); -} -_:-ms-fullscreen input[type='range']::-ms-thumb, -:root input[type='range']::-ms-thumb { + } + input[type="range"]::-ms-thumb { border: 0; margin-top: 0; border-radius: 50%; @@ -12779,1413 +12671,1445 @@ _:-ms-fullscreen input[type='range']::-ms-thumb, width: 0.7rem; background-color: #0072a3; background-color: var(--clr-forms-range-progress-fill-color, #0072a3); + } +} +_:-ms-fullscreen .clr-range-wrapper .fill-input, +:root .clr-range-wrapper .fill-input { + display: none; +} +_:-ms-fullscreen + .clr-range-wrapper.progress-fill + input[type="range"]::-ms-fill-lower, +:root .clr-range-wrapper.progress-fill input[type="range"]::-ms-fill-lower { + height: 0.2rem; + background-color: #0072a3; + background-color: var(--clr-forms-range-progress-fill-color, #0072a3); +} +_:-ms-fullscreen input[type="range"]::-ms-tooltip, +:root input[type="range"]::-ms-tooltip { + visibility: hidden; +} +_:-ms-fullscreen input[type="range"], +:root input[type="range"] { + border: 0; + margin: 0; + -webkit-appearance: none; + left: 0; + height: 0.7rem; +} +_:-ms-fullscreen input[type="range"]::-ms-track, +:root input[type="range"]::-ms-track { + border: 0; + margin: 0; + height: 0.2rem; + cursor: pointer; + background-color: #e8e8e8; + background-color: var(--clr-forms-range-track-color, #e8e8e8); +} +_:-ms-fullscreen input[type="range"]::-ms-thumb, +:root input[type="range"]::-ms-thumb { + border: 0; + margin-top: 0; + border-radius: 50%; + height: 0.7rem; + width: 0.7rem; + background-color: #0072a3; + background-color: var(--clr-forms-range-progress-fill-color, #0072a3); } input[clrDatalist]::-webkit-calendar-picker-indicator { - display: none; + display: none; } clr-datalist-container .clr-input-group { - padding-right: 0; + padding-right: 0; } :root { - --clr-calendar-background-color: var(--clr-color-neutral-0); - --clr-calendar-border-color: var(--clr-color-neutral-400); - --clr-datepicker-trigger-color: var(--clr-color-action-600); - --clr-datepicker-trigger-hover-color: var(--clr-color-action-800); - --clr-calendar-btn-color: var(--clr-color-action-600); - --clr-calendar-btn-hover-focus-color: var(--clr-global-hover-color); - --clr-calendar-picker-btn-font-size: 0.9rem; - --clr-calendar-picker-btn-font-weight: 200; - --clr-calendar-today-date-cell-color: var(--clr-color-neutral-1000); - --clr-calendar-today-date-cell-font-weight: 600; - --clr-calendar-active-cell-background-color: var( - --clr-global-selection-color - ); - --clr-calendar-active-focus-cell-background-color: var( - --clr-global-selection-color - ); - --clr-calendar-active-cell-color: var(--clr-color-neutral-1000); - --clr-day-font-size: 0.6rem; + --clr-calendar-background-color: var(--clr-color-neutral-0); + --clr-calendar-border-color: var(--clr-color-neutral-400); + --clr-datepicker-trigger-color: var(--clr-color-action-600); + --clr-datepicker-trigger-hover-color: var(--clr-color-action-800); + --clr-calendar-btn-color: var(--clr-color-action-600); + --clr-calendar-btn-hover-focus-color: var(--clr-global-hover-color); + --clr-calendar-picker-btn-font-size: 0.9rem; + --clr-calendar-picker-btn-font-weight: 200; + --clr-calendar-today-date-cell-color: var(--clr-color-neutral-1000); + --clr-calendar-today-date-cell-font-weight: 600; + --clr-calendar-active-cell-background-color: var( + --clr-global-selection-color + ); + --clr-calendar-active-focus-cell-background-color: var( + --clr-global-selection-color + ); + --clr-calendar-active-cell-color: var(--clr-color-neutral-1000); + --clr-day-font-size: 0.6rem; } .date-container { - display: -webkit-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - white-space: nowrap; + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + white-space: nowrap; } .datepicker-trigger { - height: 1.8rem; - min-width: 1.8rem; - padding: 0 0; - margin: 0 !important; - color: inherit; - border: none; - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - background: 0 0; - cursor: pointer; - line-height: 1.8rem; + height: 1.8rem; + min-width: 1.8rem; + padding: 0 0; + margin: 0 !important; + color: inherit; + border: none; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + background: 0 0; + cursor: pointer; + line-height: 1.8rem; } .datepicker-trigger .datepicker-trigger-icon { - fill: #0072a3; - fill: var(--clr-datepicker-trigger-color, #0072a3); + fill: #0072a3; + fill: var(--clr-datepicker-trigger-color, #0072a3); } .datepicker-trigger .datepicker-trigger-icon:hover { - fill: #00567a; - fill: var(--clr-datepicker-trigger-hover-color, #00567a); + fill: #00567a; + fill: var(--clr-datepicker-trigger-hover-color, #00567a); } .datepicker-trigger:disabled { - cursor: not-allowed; + cursor: not-allowed; } .clr-form-control .datepicker-trigger { - line-height: 1.1rem; - height: 1.1rem; + line-height: 1.1rem; + height: 1.1rem; } .date-input { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; } .datepicker { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - padding: 0.6rem; - margin-top: 0.3rem; - width: 13.9rem; - height: 15.7rem; - background: #fff; - background: var(--clr-calendar-background-color, #fff); - border: 0.05rem solid; - border-color: #ccc; - border-color: var(--clr-calendar-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-popover-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-popover-box-shadow-color, rgba(140, 140, 140, 0.25)); - overflow: hidden; - z-index: 1060; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + padding: 0.6rem; + margin-top: 0.3rem; + width: 13.9rem; + height: 15.7rem; + background: #fff; + background: var(--clr-calendar-background-color, #fff); + border: 0.05rem solid; + border-color: #ccc; + border-color: var(--clr-calendar-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-popover-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-popover-box-shadow-color, rgba(140, 140, 140, 0.25)); + overflow: hidden; + z-index: 1060; } .calendar-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } .calendar-switchers, .year-switchers { - -webkit-transform: translateY(-0.1rem); - transform: translateY(-0.1rem); - display: -webkit-box; - display: -ms-flexbox; - display: flex; + -webkit-transform: translateY(-0.1rem); + transform: translateY(-0.1rem); + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .year-switchers { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: 5.4rem; - -ms-flex-item-align: center; - align-self: center; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 5.4rem; + -ms-flex-item-align: center; + align-self: center; } .calendar-table { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - width: 100%; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 100%; } .calendar-cell, .calendar-table .calendar-cell { - height: 1.8rem; - width: 1.8rem; - min-height: 1.8rem; - min-width: 1.8rem; - padding: 0; - text-align: center; + height: 1.8rem; + width: 1.8rem; + min-height: 1.8rem; + min-width: 1.8rem; + padding: 0; + text-align: center; } .day { - display: inline-block; + display: inline-block; } .weekdays { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.8rem; - flex: 0 0 1.8rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.8rem; + flex: 0 0 1.8rem; } .weekday { - font-size: 0.6rem; - font-size: var(--clr-day-font-size, 0.6rem); - font-weight: 600; + font-size: 0.6rem; + font-size: var(--clr-day-font-size, 0.6rem); + font-weight: 600; } .calendar-btn { - height: 1.8rem; - min-width: 1.8rem; - padding: 0 0; - margin: 0 !important; - color: inherit; - border: none; - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - background: 0 0; - cursor: pointer; - line-height: 1.8rem; - font-size: 0.9rem; - font-size: var(--clr-calendar-picker-btn-font-size, 0.9rem); - font-weight: 200; - font-weight: var(--clr-calendar-picker-btn-font-weight, 200); + height: 1.8rem; + min-width: 1.8rem; + padding: 0 0; + margin: 0 !important; + color: inherit; + border: none; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + background: 0 0; + cursor: pointer; + line-height: 1.8rem; + font-size: 0.9rem; + font-size: var(--clr-calendar-picker-btn-font-size, 0.9rem); + font-weight: 200; + font-weight: var(--clr-calendar-picker-btn-font-weight, 200); } .calendar-btn:focus, .calendar-btn:hover { - background: #e8e8e8; - background: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); + background: #e8e8e8; + background: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); } .calendar-btn:focus { - outline: 0; + outline: 0; } .day-btn { - height: 1.8rem; - min-width: 1.8rem; - padding: 0 0; - margin: 0 !important; - color: inherit; - border: none; - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - background: 0 0; - cursor: pointer; - line-height: 1.8rem; - width: 100%; - color: inherit; + height: 1.8rem; + min-width: 1.8rem; + padding: 0 0; + margin: 0 !important; + color: inherit; + border: none; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + background: 0 0; + cursor: pointer; + line-height: 1.8rem; + width: 100%; + color: inherit; } .day-btn:focus, .day-btn:hover { - background: #e8e8e8; - background: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); + background: #e8e8e8; + background: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); } .day-btn:focus { - outline: 0; + outline: 0; } .day-btn.is-today { - color: #000; - color: var(--clr-calendar-today-date-cell-color, #000); - font-weight: 600; - font-weight: var(--clr-calendar-today-date-cell-font-weight, 600); + color: #000; + color: var(--clr-calendar-today-date-cell-color, #000); + font-weight: 600; + font-weight: var(--clr-calendar-today-date-cell-font-weight, 600); } .day-btn.is-excluded { - opacity: 0.4; + opacity: 0.4; } .day-btn.is-excluded:hover { - background: 0 0; + background: 0 0; } .day-btn.is-selected { - background: #d8e3e9; - background: var(--clr-calendar-active-cell-background-color, #d8e3e9); - color: #000; - color: var(--clr-calendar-active-cell-color, #000); + background: #d8e3e9; + background: var(--clr-calendar-active-cell-background-color, #d8e3e9); + color: #000; + color: var(--clr-calendar-active-cell-color, #000); } .day-btn.is-selected:focus { - background: #d8e3e9; - background: var(--clr-calendar-active-focus-cell-background-color, #d8e3e9); + background: #d8e3e9; + background: var(--clr-calendar-active-focus-cell-background-color, #d8e3e9); } .day-btn.is-disabled { - opacity: 0.4; - pointer-events: none; + opacity: 0.4; + pointer-events: none; } .day-btn.is-disabled:hover { - background: 0 0; + background: 0 0; } .calendar-pickers { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .switcher { - color: #0072a3; - color: var(--clr-calendar-btn-color, #0072a3); + color: #0072a3; + color: var(--clr-calendar-btn-color, #0072a3); } .switcher cds-icon { - height: 0.9rem; - width: 0.9rem; + height: 0.9rem; + width: 0.9rem; } .monthpicker-trigger, .yearpicker-trigger { - min-width: 2.4rem; - max-width: 3rem; - color: #0072a3; - color: var(--clr-calendar-btn-color, #0072a3); - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + min-width: 2.4rem; + max-width: 3rem; + color: #0072a3; + color: var(--clr-calendar-btn-color, #0072a3); + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .monthpicker, .yearpicker { - min-height: 14.4rem; + min-height: 14.4rem; } .monthpicker, .yearpicker, .years { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - overflow: hidden; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + overflow: hidden; } .monthpicker, .years { - -ms-flex-wrap: wrap; - flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; } .month, .year { - height: 2.4rem; - min-width: 2.4rem; - padding: 0 0.6rem; - margin: 0 !important; - color: inherit; - border: none; - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - background: 0 0; - cursor: pointer; - line-height: 1.8rem; - width: 6.3rem; - font-size: 0.9rem; - font-weight: 200; - outline-offset: -0.25rem; + height: 2.4rem; + min-width: 2.4rem; + padding: 0 0.6rem; + margin: 0 !important; + color: inherit; + border: none; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + background: 0 0; + cursor: pointer; + line-height: 1.8rem; + width: 6.3rem; + font-size: 0.9rem; + font-weight: 200; + outline-offset: -0.25rem; } .month:focus, .month:hover, .year:focus, .year:hover { - background: #e8e8e8; - background: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); + background: #e8e8e8; + background: var(--clr-calendar-btn-hover-focus-color, #e8e8e8); } .month:focus, .year:focus { - outline: 0; + outline: 0; } .month.is-disabled, .year.is-disabled { - opacity: 0.4; - pointer-events: none; + opacity: 0.4; + pointer-events: none; } .month.is-disabled:hover, .year.is-disabled:hover { - background: 0 0; + background: 0 0; } .month.is-selected, .year.is-selected { - background: #d8e3e9; - background: var(--clr-calendar-active-cell-background-color, #d8e3e9); - color: #000; - color: var(--clr-calendar-active-cell-color, #000); + background: #d8e3e9; + background: var(--clr-calendar-active-cell-background-color, #d8e3e9); + color: #000; + color: var(--clr-calendar-active-cell-color, #000); } .month.is-selected:focus, .year.is-selected:focus { - background: #d8e3e9; - background: var(--clr-calendar-active-focus-cell-background-color, #d8e3e9); + background: #d8e3e9; + background: var(--clr-calendar-active-focus-cell-background-color, #d8e3e9); } .month { - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .year { - text-align: center; + text-align: center; } :root { - --clr-combobox-trigger-color: var(--clr-color-neutral-500); - --clr-combobox-font-size: 0.65rem; - --clr-combobox-border-color: #e8e8e8; - --clr-combobox-border-radius: 0.15rem; - --clr-combobox-input-background: #f2f2f2; - --clr-combobox-pill-background-color: white; - --clr-combobox-pill-border-color: #e8e8e8; - --clr-combobox-pill-border-radius: 0.15rem; - --clr-combobox-pill-font-color: #454545; - --clr-combobox-filter-highlight: #454545; + --clr-combobox-trigger-color: var(--clr-color-neutral-500); + --clr-combobox-font-size: 0.65rem; + --clr-combobox-border-color: #e8e8e8; + --clr-combobox-border-radius: 0.15rem; + --clr-combobox-input-background: #f2f2f2; + --clr-combobox-pill-background-color: white; + --clr-combobox-pill-border-color: #e8e8e8; + --clr-combobox-pill-border-radius: 0.15rem; + --clr-combobox-pill-font-color: #454545; + --clr-combobox-filter-highlight: #454545; } .clr-combobox-form-control { - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; } .clr-combobox-form-control .clr-error .clr-validate-icon { - margin-left: 0; + margin-left: 0; } .clr-focus-indicator { - background-color: #0072a3; - background-color: var(--clr-forms-focused-color, #0072a3); - height: 0.1rem; - width: 0; - -webkit-transition: width 0.2s ease; - transition: width 0.2s ease; - position: absolute; - bottom: -0.05rem; - left: 0; - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-combobox-border-radius, 0.15rem); - border-bottom-right-radius: 0.15rem; - border-bottom-right-radius: var(--clr-combobox-border-radius, 0.15rem); + background-color: #0072a3; + background-color: var(--clr-forms-focused-color, #0072a3); + height: 0.1rem; + width: 0; + -webkit-transition: width 0.2s ease; + transition: width 0.2s ease; + position: absolute; + bottom: -0.05rem; + left: 0; + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-combobox-border-radius, 0.15rem); + border-bottom-right-radius: 0.15rem; + border-bottom-right-radius: var(--clr-combobox-border-radius, 0.15rem); } .clr-focus { - width: 100%; + width: 100%; } .clr-error .clr-focus-indicator { - background-color: #c21d00; - background-color: var(--clr-forms-invalid-color, #c21d00); + background-color: #c21d00; + background-color: var(--clr-forms-invalid-color, #c21d00); } .clr-combobox-wrapper { - position: relative; - padding: 0 1.2rem 0 0.3rem; - min-height: 1.2rem; - min-width: 8.4rem; - font-size: 0.65rem; - font-size: var(--clr-combobox-font-size, 0.65rem); - color: #000; - color: var(--clr-forms-text-color, #000); - border-bottom: 0.05rem solid; - border-bottom-color: #b3b3b3; - border-bottom-color: var(--clr-forms-border-color, #b3b3b3); - display: inline-block; - background-color: #f2f2f2; - background-color: var(--clr-combobox-input-background-color, #f2f2f2); - border: 0.05rem solid #e8e8e8; - border-bottom: 0.05rem solid #b3b3b3; - border-radius: 0.15rem; - border-radius: var(--clr-combobox-border-radius, 0.15rem); + position: relative; + padding: 0 1.2rem 0 0.3rem; + min-height: 1.2rem; + min-width: 8.4rem; + font-size: 0.65rem; + font-size: var(--clr-combobox-font-size, 0.65rem); + color: #000; + color: var(--clr-forms-text-color, #000); + border-bottom: 0.05rem solid; + border-bottom-color: #b3b3b3; + border-bottom-color: var(--clr-forms-border-color, #b3b3b3); + display: inline-block; + background-color: #f2f2f2; + background-color: var(--clr-combobox-input-background-color, #f2f2f2); + border: 0.05rem solid #e8e8e8; + border-bottom: 0.05rem solid #b3b3b3; + border-radius: 0.15rem; + border-radius: var(--clr-combobox-border-radius, 0.15rem); } .clr-combobox-wrapper.multi { - min-width: 18rem; - padding-bottom: 0.15rem; + min-width: 18rem; + padding-bottom: 0.15rem; } .clr-combobox-wrapper .clr-input.clr-combobox-input:focus { - background: 0 0; + background: 0 0; } .clr-combobox-wrapper.invalid { - border-bottom-color: #c21d00; + border-bottom-color: #c21d00; } .clr-combobox-wrapper .clr-combobox-input { - background: 0 0; - border-bottom: none; + background: 0 0; + border-bottom: none; } .clr-combobox-wrapper .clr-combobox-remove-btn { - background: 0 0; - border: none; - cursor: pointer; - padding: 0.15rem 0.15rem; - color: #454545; + background: 0 0; + border: none; + cursor: pointer; + padding: 0.15rem 0.15rem; + color: #454545; } .clr-combobox-wrapper .label-combobox-pill { - margin: 0.15rem 0.3rem 0 0; - background-color: #fff; - background-color: var(--clr-combobox-pill-background-color, #fff); - border-width: 0.05rem; - border-style: solid; - -webkit-box-align: baseline; - -ms-flex-align: baseline; - align-items: baseline; - border-radius: 0.15rem; - border-radius: var(--clr-combobox-pill-border-radius, 0.15rem); - border-color: #e8e8e8; - border-color: var(--clr-combobox-pill-border-color, #e8e8e8); - padding: 0 0.1rem 0 0.2rem; + margin: 0.15rem 0.3rem 0 0; + background-color: #fff; + background-color: var(--clr-combobox-pill-background-color, #fff); + border-width: 0.05rem; + border-style: solid; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; + border-radius: 0.15rem; + border-radius: var(--clr-combobox-pill-border-radius, 0.15rem); + border-color: #e8e8e8; + border-color: var(--clr-combobox-pill-border-color, #e8e8e8); + padding: 0 0.1rem 0 0.2rem; } .clr-combobox-wrapper .label-combobox-pill .clr-combobox-pill-content { - color: #454545; - color: var(--clr-combobox-pill-font-color, #454545); - font-size: 0.55rem; - font-weight: 400; - padding: 0 0.4rem 0 0.2rem; + color: #454545; + color: var(--clr-combobox-pill-font-color, #454545); + font-size: 0.55rem; + font-weight: 400; + padding: 0 0.4rem 0 0.2rem; } -.clr-combobox-wrapper .label-combobox-pill cds-icon[shape='window-close'] { - color: #454545; - color: var(--clr-combobox-pill-font-color, #454545); +.clr-combobox-wrapper .label-combobox-pill cds-icon[shape="window-close"] { + color: #454545; + color: var(--clr-combobox-pill-font-color, #454545); } .clr-combobox-wrapper .clr-combobox-input-wrapper { - border: none; - background: 0 0; + border: none; + background: 0 0; } .clr-combobox-trigger { - width: 1.2rem; - margin: auto; - position: absolute; - top: 0; - bottom: 0; - right: 0; - background: 0 0; - border: none; - color: currentColor; - cursor: pointer; - outline: 0; + width: 1.2rem; + margin: auto; + position: absolute; + top: 0; + bottom: 0; + right: 0; + background: 0 0; + border: none; + color: currentColor; + cursor: pointer; + outline: 0; } .clr-combobox-trigger:disabled { - color: var(--clr-btn-link-disabled-color, #666); - opacity: 0.4; + color: var(--clr-btn-link-disabled-color, #666); + opacity: 0.4; } -.clr-combobox-trigger cds-icon[shape='angle'] { - color: #454545; - color: var(--clr-combobox-pill-font-color, #454545); +.clr-combobox-trigger cds-icon[shape="angle"] { + color: #454545; + color: var(--clr-combobox-pill-font-color, #454545); } .clr-combobox-options { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - position: absolute; - top: 100%; - left: 0; - min-width: 6rem; - max-width: 18rem; - background: #fff; - background: var(--clr-dropdown-bg-color, #fff); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-dropdown-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); - margin-top: 0.1rem; - padding: 0.6rem 0; - visibility: hidden; - z-index: 1060; - position: static; - max-height: 12rem; - overflow-y: scroll; - visibility: visible; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + position: absolute; + top: 100%; + left: 0; + min-width: 6rem; + max-width: 18rem; + background: #fff; + background: var(--clr-dropdown-bg-color, #fff); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-dropdown-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-dropdown-box-shadow, rgba(140, 140, 140, 0.25)); + margin-top: 0.1rem; + padding: 0.6rem 0; + visibility: hidden; + z-index: 1060; + position: static; + max-height: 12rem; + overflow-y: scroll; + visibility: visible; } .clr-combobox-options .clr-combobox-options-loading { - padding: 0.15rem 1.2rem; + padding: 0.15rem 1.2rem; } .clr-combobox-options - .clr-combobox-options-loading - .clr-combobox-options-loading-text { - padding-left: 0.3rem; + .clr-combobox-options-loading + .clr-combobox-options-loading-text { + padding-left: 0.3rem; } .clr-combobox-options .clr-combobox-options-empty-text { - padding-left: 0.6rem; + padding-left: 0.6rem; } .clr-combobox-option { - color: #666; - color: var(--clr-dropdown-item-color, #666); - font-size: 0.7rem; - font-weight: 400; - font-weight: var(--clr-dropdown-item-font-weight, 400); - letter-spacing: normal; - background: 0 0; - border: 0; - cursor: pointer; - display: block; - height: auto; - line-height: inherit; - margin: 0; - width: 100%; - text-transform: none; + color: #666; + color: var(--clr-dropdown-item-color, #666); + font-size: 0.7rem; + font-weight: 400; + font-weight: var(--clr-dropdown-item-font-weight, 400); + letter-spacing: normal; + background: 0 0; + border: 0; + cursor: pointer; + display: block; + height: auto; + line-height: inherit; + margin: 0; + width: 100%; + text-transform: none; } .clr-combobox-option:hover { - background-color: #e8e8e8; - background-color: var(--clr-dropdown-bg-hover-color, #e8e8e8); - color: #666; - color: var(--clr-dropdown-item-color, #666); - text-decoration: none; + background-color: #e8e8e8; + background-color: var(--clr-dropdown-bg-hover-color, #e8e8e8); + color: #666; + color: var(--clr-dropdown-item-color, #666); + text-decoration: none; } .clr-combobox-option.active { - background: #d8e3e9; - background: var(--clr-dropdown-selection-color, #d8e3e9); - color: #000; - color: var(--clr-dropdown-active-text-color, #000); + background: #d8e3e9; + background: var(--clr-dropdown-selection-color, #d8e3e9); + color: #000; + color: var(--clr-dropdown-active-text-color, #000); } .clr-combobox-option:active { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .clr-combobox-option:focus { - z-index: inherit; + z-index: inherit; } .clr-combobox-option.disabled, .clr-combobox-option:disabled { - cursor: not-allowed; - opacity: 0.4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: not-allowed; + opacity: 0.4; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .clr-combobox-option.disabled:hover, .clr-combobox-option:disabled:hover { - background: 0 0; + background: 0 0; } .clr-combobox-option.disabled:active, .clr-combobox-option:disabled:active { - background: 0 0; - -webkit-box-shadow: none; - box-shadow: none; + background: 0 0; + -webkit-box-shadow: none; + box-shadow: none; } .clr-combobox-option.clr-focus { - background-color: #e8e8e8; - background-color: var(--clr-dropdown-bg-hover-color, #e8e8e8); + background-color: #e8e8e8; + background-color: var(--clr-dropdown-bg-hover-color, #e8e8e8); } .clr-combobox-options .clr-combobox-option { - padding: 0.15rem 1.2rem; + padding: 0.15rem 1.2rem; } @media screen and (max-width: 576px) { - .clr-combobox-options .clr-combobox-option { - padding: 0.3rem 1.2rem; - } + .clr-combobox-options .clr-combobox-option { + padding: 0.3rem 1.2rem; + } } .clr-combobox-options .clr-combobox-option.active, .clr-combobox-options .clr-combobox-option:hover { - color: #666; - color: var(--clr-dropdown-item-color, #666); + color: #666; + color: var(--clr-dropdown-item-color, #666); } .clr-combobox-disabled { - color: #b3b3b3; - color: var(--clr-forms-text-disabled-color, #b3b3b3); - cursor: not-allowed; + color: #b3b3b3; + color: var(--clr-forms-text-disabled-color, #b3b3b3); + cursor: not-allowed; } .clr-combobox-disabled .clr-combobox-input, .clr-combobox-disabled .clr-combobox-remove-btn, .clr-combobox-disabled .clr-combobox-trigger { - cursor: not-allowed; + cursor: not-allowed; } .clr-filter-highlight b { - font-weight: 500; - color: #454545; - color: var(--clr-combobox-filter-highlight, #454545); + font-weight: 500; + color: #454545; + color: var(--clr-combobox-filter-highlight, #454545); } :root { - --clr-stack-view-border-radius: 0.15rem; - --clr-stack-view-border-color: var(--clr-color-neutral-400); - --clr-stack-view-bg-color: var(--clr-color-neutral-50); - --clr-stack-view-stack-block-border-bottom: var(--clr-color-neutral-300); - --clr-stack-view-color: #666666; - --clr-stack-view-stack-block-label-text-color: var(--clr-global-font-color); - --clr-stack-view-border-box-color: var(--clr-color-neutral-300); - --clr-stack-block-changed-border-top-color: var(--clr-color-action-600); - --clr-stack-view-stack-block-label-and-content-bg-color: var( - --clr-color-neutral-50 - ); - --clr-stack-view-stack-children-stack-block-border-bottom-color: var( - --clr-color-neutral-200 - ); - --clr-stack-view-stack-children-stack-block-label-and-content-bg-color: var( - --clr-color-neutral-0 - ); - --clr-stack-view-stack-block-expanded-bg-color: var( - --clr-global-selection-color - ); - --clr-stack-view-stack-block-expandable-hover: var(--clr-color-neutral-200); - --clr-stack-view-stack-block-content-text-color: inherit; - --clr-stack-view-stack-block-expanded-text-color: var( - --clr-color-neutral-1000 - ); - --clr-stack-view-stack-block-caret-color: var(--clr-global-font-color); + --clr-stack-view-border-radius: 0.15rem; + --clr-stack-view-border-color: var(--clr-color-neutral-400); + --clr-stack-view-bg-color: var(--clr-color-neutral-50); + --clr-stack-view-stack-block-border-bottom: var(--clr-color-neutral-300); + --clr-stack-view-color: #666666; + --clr-stack-view-stack-block-label-text-color: var(--clr-global-font-color); + --clr-stack-view-border-box-color: var(--clr-color-neutral-300); + --clr-stack-block-changed-border-top-color: var(--clr-color-action-600); + --clr-stack-view-stack-block-label-and-content-bg-color: var( + --clr-color-neutral-50 + ); + --clr-stack-view-stack-children-stack-block-border-bottom-color: var( + --clr-color-neutral-200 + ); + --clr-stack-view-stack-children-stack-block-label-and-content-bg-color: var( + --clr-color-neutral-0 + ); + --clr-stack-view-stack-block-expanded-bg-color: var( + --clr-global-selection-color + ); + --clr-stack-view-stack-block-expandable-hover: var(--clr-color-neutral-200); + --clr-stack-view-stack-block-content-text-color: inherit; + --clr-stack-view-stack-block-expanded-text-color: var( + --clr-color-neutral-1000 + ); + --clr-stack-view-stack-block-caret-color: var(--clr-global-font-color); } .stack-header { - font-weight: 400; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: end; - -ms-flex-align: end; - align-items: flex-end; + font-weight: 400; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: flex-end; } .stack-header .stack-title { - display: block; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - padding: 0.3rem 0; + display: block; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 0.3rem 0; } .stack-header .stack-actions { - display: block; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; + display: block; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; } .stack-header .stack-actions .stack-action { - margin: 0 0 0.3rem 0.6rem; + margin: 0 0 0.3rem 0.6rem; } .stack-header .stack-actions .stack-action.btn { - min-width: 0; - padding: 0 0.6rem; + min-width: 0; + padding: 0 0.6rem; } .stack-header .stack-actions .stack-action.btn-link { - margin-right: -0.6rem; + margin-right: -0.6rem; } .stack-view { - color: #666; - color: var(--clr-stack-view-color, #666); - font-size: 0.65rem; - font-weight: 400; - line-height: 1.2rem; - letter-spacing: normal; - margin-top: 0; - border-width: 0.05rem; - border-style: solid; - border-color: #ccc; - border-color: var(--clr-stack-view-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-stack-view-border-radius, 0.15rem); - overflow-y: auto; - background-color: #fafafa; - background-color: var(--clr-stack-view-bg-color, #fafafa); - word-wrap: break-word; - -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC); + color: #666; + color: var(--clr-stack-view-color, #666); + font-size: 0.65rem; + font-weight: 400; + line-height: 1.2rem; + letter-spacing: normal; + margin-top: 0; + border-width: 0.05rem; + border-style: solid; + border-color: #ccc; + border-color: var(--clr-stack-view-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-stack-view-border-radius, 0.15rem); + overflow-y: auto; + background-color: #fafafa; + background-color: var(--clr-stack-view-bg-color, #fafafa); + word-wrap: break-word; + -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC); } .stack-view dd, .stack-view dt { - -webkit-margin-start: 0; - -moz-margin-start: 0; - margin-inline-start: 0; - margin-left: 0; + -webkit-margin-start: 0; + -moz-margin-start: 0; + margin-inline-start: 0; + margin-left: 0; } .stack-view .stack-block { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row wrap; - flex-flow: row wrap; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-global-borderwidth, 0.05rem); - border-bottom-style: solid; - border-bottom-color: #dedede; - border-bottom-color: var( - --clr-stack-view-stack-block-border-bottom, - #dedede - ); - overflow-y: hidden; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-global-borderwidth, 0.05rem); + border-bottom-style: solid; + border-bottom-color: #dedede; + border-bottom-color: var(--clr-stack-view-stack-block-border-bottom, #dedede); + overflow-y: hidden; } .stack-view > .stack-block:last-child, .stack-view > :last-child .stack-block:last-of-type { - border-bottom: none; - -webkit-box-shadow: 0 0.05rem 0 #dedede; - box-shadow: 0 0.05rem 0 #dedede; - -webkit-box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); - box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); + border-bottom: none; + -webkit-box-shadow: 0 0.05rem 0 #dedede; + box-shadow: 0 0.05rem 0 #dedede; + -webkit-box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); + box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); } .stack-view .stack-block-changed > .stack-block-label { - margin-left: -0.45rem; + margin-left: -0.45rem; } .stack-view .stack-block-changed::before { - content: ' '; - position: relative; - height: 0; - width: 0; - border-top: 0.45rem solid; - border-top-color: #0072a3; - border-top-color: var(--clr-stack-block-changed-border-top-color, #0072a3); - border-right: 0.45rem solid transparent; + content: " "; + position: relative; + height: 0; + width: 0; + border-top: 0.45rem solid; + border-top-color: #0072a3; + border-top-color: var(--clr-stack-block-changed-border-top-color, #0072a3); + border-right: 0.45rem solid transparent; } .stack-view .stack-block-label { - padding: 0.3rem 0.6rem; - background-color: #fafafa; - background-color: var( - --clr-stack-view-stack-block-label-and-content-bg-color, - #fafafa - ); + padding: 0.3rem 0.6rem; + background-color: #fafafa; + background-color: var( + --clr-stack-view-stack-block-label-and-content-bg-color, + #fafafa + ); } .stack-view .stack-block-content { - background-color: inherit; + background-color: inherit; } .stack-view .stack-block-caret { - -ms-flex-item-align: center; - align-self: center; + -ms-flex-item-align: center; + align-self: center; } -.stack-view .stack-block-label { - color: #666; - color: var(--clr-stack-view-stack-block-label-text-color, #666); - font-size: 0.65rem; - font-weight: 500; - line-height: 1.2rem; - letter-spacing: normal; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; +.stack-view .stack-block-label { + color: #666; + color: var(--clr-stack-view-stack-block-label-text-color, #666); + font-size: 0.65rem; + font-weight: 500; + line-height: 1.2rem; + letter-spacing: normal; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .stack-view .stack-block-label::before { - display: inline-block; - content: ''; - float: left; - height: 0.7rem; - width: 0.7rem; - margin: 0.35rem 0.24rem 0 0; - text-align: center; + display: inline-block; + content: ""; + float: left; + height: 0.7rem; + width: 0.7rem; + margin: 0.35rem 0.24rem 0 0; + text-align: center; } .stack-view .stack-block-label:focus { - outline: 0.25rem auto -webkit-focus-ring-color; + outline: 0.25rem auto -webkit-focus-ring-color; } .stack-view .stack-view-key { - -webkit-box-flex: 0; - -ms-flex: 0 0 40%; - flex: 0 0 40%; - max-width: 40%; + -webkit-box-flex: 0; + -ms-flex: 0 0 40%; + flex: 0 0 40%; + max-width: 40%; } .stack-view .stack-block-caret { - height: 0.7rem; - width: 0.7rem; - margin-right: 0.24rem; - fill: #666; - fill: var(--clr-stack-view-stack-block-caret-color, #666); + height: 0.7rem; + width: 0.7rem; + margin-right: 0.24rem; + fill: #666; + fill: var(--clr-stack-view-stack-block-caret-color, #666); } .stack-view .stack-block-content { - color: inherit; - color: var(--clr-stack-view-stack-block-content-text-color, inherit); - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - width: 60%; - margin-bottom: 0; - font-weight: 400; + color: inherit; + color: var(--clr-stack-view-stack-block-content-text-color, inherit); + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 60%; + margin-bottom: 0; + font-weight: 400; } .stack-view .stack-block-content > :first-child { - margin-top: 0; + margin-top: 0; } .stack-view .stack-block-content > :last-child { - margin-bottom: 0; + margin-bottom: 0; } .stack-view .stack-children { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: 100%; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; } .stack-view .stack-children .stack-block { - border-bottom-color: #e8e8e8; - border-bottom-color: var( - --clr-stack-view-stack-children-stack-block-border-bottom-color, - #e8e8e8 - ); + border-bottom-color: #e8e8e8; + border-bottom-color: var( + --clr-stack-view-stack-children-stack-block-border-bottom-color, + #e8e8e8 + ); } .stack-view .stack-children > .stack-block:last-child, .stack-view .stack-children > :last-child .stack-block:last-of-type { - border-bottom: none; - -webkit-box-shadow: 0 0.05rem 0 #dedede; - box-shadow: 0 0.05rem 0 #dedede; - -webkit-box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); - box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); + border-bottom: none; + -webkit-box-shadow: 0 0.05rem 0 #dedede; + box-shadow: 0 0.05rem 0 #dedede; + -webkit-box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); + box-shadow: 0 0.05rem 0 var(--clr-stack-view-border-box-color); } .stack-view .stack-children .stack-block-content, .stack-view .stack-children .stack-block-label { - background-color: #fff; - background-color: var( - --clr-stack-view-stack-children-stack-block-label-and-content-bg-color, - #fff - ); + background-color: #fff; + background-color: var( + --clr-stack-view-stack-children-stack-block-label-and-content-bg-color, + #fff + ); } .stack-view .stack-children .stack-block-label { - padding-left: 1.2rem; + padding-left: 1.2rem; } .stack-view .stack-block-expandable > .stack-block-label { - cursor: pointer; + cursor: pointer; } .stack-view .stack-block-expandable > .stack-block-label::before { - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%23666666%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A'); - background-repeat: no-repeat; - background-size: contain; - vertical-align: middle; - -webkit-transform: rotate(-90deg); - transform: rotate(-90deg); - height: 0.6rem; - width: 0.6rem; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2012%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E.cls-1%7Bfill%3A%23666666%3B%7D%3C%2Fstyle%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Ctitle%3ECaret%3C%2Ftitle%3E%0A%20%20%20%20%3Cpath%20class%3D%22cls-1%22%20d%3D%22M6%2C9L1.2%2C4.2a0.68%2C0.68%2C0%2C0%2C1%2C1-1L6%2C7.08%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A"); + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; + -webkit-transform: rotate(-90deg); + transform: rotate(-90deg); + height: 0.6rem; + width: 0.6rem; } .stack-view .stack-block-expandable > .stack-block-content, .stack-view .stack-block-expandable > .stack-block-label { - -webkit-transition: - background-color 0.2s ease-in-out, - color 0.2s ease-in-out; - transition: - background-color 0.2s ease-in-out, - color 0.2s ease-in-out; + -webkit-transition: + background-color 0.2s ease-in-out, + color 0.2s ease-in-out; + transition: + background-color 0.2s ease-in-out, + color 0.2s ease-in-out; } .stack-view - .stack-block-expandable:hover:not(.stack-block-expanded) - > .stack-block-label { - background-color: #e8e8e8; - background-color: var( - --clr-stack-view-stack-block-expandable-hover, - #e8e8e8 - ); + .stack-block-expandable:hover:not(.stack-block-expanded) + > .stack-block-label { + background-color: #e8e8e8; + background-color: var(--clr-stack-view-stack-block-expandable-hover, #e8e8e8); } .stack-view .stack-block-expanded > .stack-block-label::before { - -webkit-transform: rotate(0); - transform: rotate(0); + -webkit-transform: rotate(0); + transform: rotate(0); } .stack-view .stack-block-expanded > .stack-block-label { - background-color: #d8e3e9; - background-color: var( - --clr-stack-view-stack-block-expanded-bg-color, - #d8e3e9 - ); - color: #000; - color: var(--clr-stack-view-stack-block-expanded-text-color, #000); + background-color: #d8e3e9; + background-color: var( + --clr-stack-view-stack-block-expanded-bg-color, + #d8e3e9 + ); + color: #000; + color: var(--clr-stack-view-stack-block-expanded-text-color, #000); } .stack-view .select, -.stack-view input[type='date'], -.stack-view input[type='datetime-local'], -.stack-view input[type='email'], -.stack-view input[type='number'], -.stack-view input[type='password'], -.stack-view input[type='tel'], -.stack-view input[type='text'], -.stack-view input[type='time'], -.stack-view input[type='url'] { - display: inline-block; - vertical-align: top; - margin-right: 0.6rem; - margin-bottom: -0.95rem; +.stack-view input[type="date"], +.stack-view input[type="datetime-local"], +.stack-view input[type="email"], +.stack-view input[type="number"], +.stack-view input[type="password"], +.stack-view input[type="tel"], +.stack-view input[type="text"], +.stack-view input[type="time"], +.stack-view input[type="url"] { + display: inline-block; + vertical-align: top; + margin-right: 0.6rem; + margin-bottom: -0.95rem; } .stack-view .select select, -.stack-view input[type='date'], -.stack-view input[type='datetime-local'], -.stack-view input[type='email'], -.stack-view input[type='number'], -.stack-view input[type='password'], -.stack-view input[type='tel'], -.stack-view input[type='text'], -.stack-view input[type='time'], -.stack-view input[type='url'] { - height: 1.2rem; -} -.stack-view .stack-block-expandable > .stack-block-content input[type='date'], +.stack-view input[type="date"], +.stack-view input[type="datetime-local"], +.stack-view input[type="email"], +.stack-view input[type="number"], +.stack-view input[type="password"], +.stack-view input[type="tel"], +.stack-view input[type="text"], +.stack-view input[type="time"], +.stack-view input[type="url"] { + height: 1.2rem; +} +.stack-view .stack-block-expandable > .stack-block-content input[type="date"], .stack-view - .stack-block-expandable - > .stack-block-content - input[type='datetime-local'], -.stack-view .stack-block-expandable > .stack-block-content input[type='email'], -.stack-view .stack-block-expandable > .stack-block-content input[type='number'], + .stack-block-expandable + > .stack-block-content + input[type="datetime-local"], +.stack-view .stack-block-expandable > .stack-block-content input[type="email"], +.stack-view .stack-block-expandable > .stack-block-content input[type="number"], .stack-view - .stack-block-expandable - > .stack-block-content - input[type='password'], -.stack-view .stack-block-expandable > .stack-block-content input[type='tel'], -.stack-view .stack-block-expandable > .stack-block-content input[type='text'], -.stack-view .stack-block-expandable > .stack-block-content input[type='time'], -.stack-view .stack-block-expandable > .stack-block-content input[type='url'] { - -webkit-transition: - background-size 0.2s ease, - border-bottom-color 0.2s ease-in-out; - transition: - background-size 0.2s ease, - border-bottom-color 0.2s ease-in-out; + .stack-block-expandable + > .stack-block-content + input[type="password"], +.stack-view .stack-block-expandable > .stack-block-content input[type="tel"], +.stack-view .stack-block-expandable > .stack-block-content input[type="text"], +.stack-view .stack-block-expandable > .stack-block-content input[type="time"], +.stack-view .stack-block-expandable > .stack-block-content input[type="url"] { + -webkit-transition: + background-size 0.2s ease, + border-bottom-color 0.2s ease-in-out; + transition: + background-size 0.2s ease, + border-bottom-color 0.2s ease-in-out; } .stack-view .stack-block-expandable > .stack-block-content .select select { - -webkit-transition: border-bottom-color 0.2s ease-in-out; - transition: border-bottom-color 0.2s ease-in-out; + -webkit-transition: border-bottom-color 0.2s ease-in-out; + transition: border-bottom-color 0.2s ease-in-out; } .stack-view .stack-block-expandable > .stack-block-content .select::after { - -webkit-transition: color 0.2s ease-in-out; - transition: color 0.2s ease-in-out; + -webkit-transition: color 0.2s ease-in-out; + transition: color 0.2s ease-in-out; } -.stack-view .stack-block-expanded > .stack-block-content input[type='date'], +.stack-view .stack-block-expanded > .stack-block-content input[type="date"], .stack-view - .stack-block-expanded - > .stack-block-content - input[type='datetime-local'], -.stack-view .stack-block-expanded > .stack-block-content input[type='email'], -.stack-view .stack-block-expanded > .stack-block-content input[type='number'], -.stack-view .stack-block-expanded > .stack-block-content input[type='password'], -.stack-view .stack-block-expanded > .stack-block-content input[type='tel'], -.stack-view .stack-block-expanded > .stack-block-content input[type='text'], -.stack-view .stack-block-expanded > .stack-block-content input[type='time'], -.stack-view .stack-block-expanded > .stack-block-content input[type='url'] { - border-bottom-color: #8c8c8c; - border-bottom-color: var(--clr-color-neutral-600, #8c8c8c); - background: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(95%, transparent), - color-stop(95%, var(--clr-color-action-600, #0072a3)) - ) - no-repeat; - background: linear-gradient( - to bottom, - transparent 95%, - var(--clr-color-action-600, #0072a3) 95% - ) - no-repeat; - background-size: 0 100%; - -webkit-transition: background-size 0.2s ease; - transition: background-size 0.2s ease; + .stack-block-expanded + > .stack-block-content + input[type="datetime-local"], +.stack-view .stack-block-expanded > .stack-block-content input[type="email"], +.stack-view .stack-block-expanded > .stack-block-content input[type="number"], +.stack-view .stack-block-expanded > .stack-block-content input[type="password"], +.stack-view .stack-block-expanded > .stack-block-content input[type="tel"], +.stack-view .stack-block-expanded > .stack-block-content input[type="text"], +.stack-view .stack-block-expanded > .stack-block-content input[type="time"], +.stack-view .stack-block-expanded > .stack-block-content input[type="url"] { + border-bottom-color: #8c8c8c; + border-bottom-color: var(--clr-color-neutral-600, #8c8c8c); + background: -webkit-gradient( + linear, + left top, + left bottom, + color-stop(95%, transparent), + color-stop(95%, var(--clr-color-action-600, #0072a3)) + ) + no-repeat; + background: linear-gradient( + to bottom, + transparent 95%, + var(--clr-color-action-600, #0072a3) 95% + ) + no-repeat; + background-size: 0 100%; + -webkit-transition: background-size 0.2s ease; + transition: background-size 0.2s ease; } .stack-view - .stack-block-expanded - > .stack-block-content - input[type='date'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="date"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='date']:focus, + .stack-block-expanded + > .stack-block-content + input[type="date"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='datetime-local'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="datetime-local"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='datetime-local']:focus, + .stack-block-expanded + > .stack-block-content + input[type="datetime-local"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='email'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="email"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='email']:focus, + .stack-block-expanded + > .stack-block-content + input[type="email"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='number'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="number"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='number']:focus, + .stack-block-expanded + > .stack-block-content + input[type="number"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='password'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="password"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='password']:focus, + .stack-block-expanded + > .stack-block-content + input[type="password"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='tel'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="tel"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='tel']:focus, + .stack-block-expanded + > .stack-block-content + input[type="tel"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='text'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="text"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='text']:focus, + .stack-block-expanded + > .stack-block-content + input[type="text"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='time'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="time"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='time']:focus, + .stack-block-expanded + > .stack-block-content + input[type="time"]:focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='url'].clr-focus, + .stack-block-expanded + > .stack-block-content + input[type="url"].clr-focus, .stack-view - .stack-block-expanded - > .stack-block-content - input[type='url']:focus { - border-bottom-color: #0072a3; - border-bottom-color: var(--clr-color-action-600, #0072a3); - background-size: 100% 100%; + .stack-block-expanded + > .stack-block-content + input[type="url"]:focus { + border-bottom-color: #0072a3; + border-bottom-color: var(--clr-color-action-600, #0072a3); + background-size: 100% 100%; } .stack-view .stack-block-expanded > .stack-block-content .select select { - border-bottom-color: #8c8c8c; - border-bottom-color: var(--clr-color-neutral-600, #8c8c8c); + border-bottom-color: #8c8c8c; + border-bottom-color: var(--clr-color-neutral-600, #8c8c8c); } .stack-view .stack-block-expanded > .stack-block-content .select::after { - color: #8c8c8c; - color: var(--clr-color-neutral-600, #8c8c8c); + color: #8c8c8c; + color: var(--clr-color-neutral-600, #8c8c8c); } .modal .stack-view { - height: 55vh; - margin-bottom: 0; + height: 55vh; + margin-bottom: 0; } .stack-view clr-stack-block.stack-block-expandable .stack-block-label::before { - content: none; + content: none; } .stack-view .stack-children .stack-block-label, .stack-view .stack-children clr-stack-block .stack-block-label { - padding-left: 1.8rem; + padding-left: 1.8rem; } .stack-view .stack-children .stack-block-label::before, .stack-view .stack-children clr-stack-block .stack-block-label::before { - display: none; + display: none; } .stack-view .stack-children .stack-block-content, .stack-view .stack-children clr-stack-block .stack-block-content { - padding-left: 0.36rem; + padding-left: 0.36rem; } :root { - --clr-tree-border-radius: var(--clr-global-borderradius); - --clr-tree-node-caret-link-hover-color: var(--clr-color-neutral-1000); - --clr-tree-link-hover-color: var(--clr-custom-links-hover-color); - --clr-tree-link-selection-color: var(--clr-global-selection-color); - --clr-tree-link-text-color: var(--clr-color-neutral-700); - --clr-tree-node-caret-color: var(--clr-color-neutral-500); + --clr-tree-border-radius: var(--clr-global-borderradius); + --clr-tree-node-caret-link-hover-color: var(--clr-color-neutral-1000); + --clr-tree-link-hover-color: var(--clr-custom-links-hover-color); + --clr-tree-link-selection-color: var(--clr-global-selection-color); + --clr-tree-link-text-color: var(--clr-color-neutral-700); + --clr-tree-node-caret-color: var(--clr-color-neutral-500); } .clr-tree-node { - display: block; + display: block; } .clr-tree-node-content-container { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } .clr-treenode-content { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - border-radius: 0; - border-top-left-radius: 0.15rem; - border-top-left-radius: var(--clr-tree-border-radius, 0.15rem); - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-tree-border-radius, 0.15rem); - line-height: 1.62rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 0; + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-tree-border-radius, 0.15rem); + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-tree-border-radius, 0.15rem); + line-height: 1.62rem; } .clr-treenode-content:first-child { - padding-left: 1.62rem; + padding-left: 1.62rem; } .clr-treenode-content cds-icon { - height: 0.8rem; - width: 0.8rem; - margin-right: 0.3rem; - vertical-align: middle; + height: 0.8rem; + width: 0.8rem; + margin-right: 0.3rem; + vertical-align: middle; } .clr-treenode-caret { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.62rem; - flex: 0 0 1.62rem; - padding: 0; - margin: 0; - height: 1.62rem; - width: 1.62rem; - background: 0 0; - border: none; - color: #b3b3b3; - color: var(--clr-tree-node-caret-color, #b3b3b3); - cursor: pointer; - outline-offset: -0.25rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.62rem; + flex: 0 0 1.62rem; + padding: 0; + margin: 0; + height: 1.62rem; + width: 1.62rem; + background: 0 0; + border: none; + color: #b3b3b3; + color: var(--clr-tree-node-caret-color, #b3b3b3); + cursor: pointer; + outline-offset: -0.25rem; } .clr-treenode-caret:hover { - color: #000; - color: var(--clr-tree-node-caret-link-hover-color, #000); + color: #000; + color: var(--clr-tree-node-caret-link-hover-color, #000); } .clr-tree-node-caret-icon { - height: 0.8rem; - width: 0.8rem; - vertical-align: middle; + height: 0.8rem; + width: 0.8rem; + vertical-align: middle; } .clr-treenode-spinner-container { - height: 1.62rem; - width: 1.62rem; - padding: 0.41rem; + height: 1.62rem; + width: 1.62rem; + padding: 0.41rem; } .clr-treenode-spinner { - height: 0.8rem; - width: 0.8rem; - min-height: 0.8rem; - min-width: 0.8rem; + height: 0.8rem; + width: 0.8rem; + min-height: 0.8rem; + min-width: 0.8rem; } .clr-treenode-children { - margin-left: 1.17rem; - will-change: height; - overflow-y: hidden; + margin-left: 1.17rem; + will-change: height; + overflow-y: hidden; } .clr-treenode-link { - display: inline-block; - height: 100%; - width: 100%; - margin: 0; - padding: 0 0 0 0.3rem; - background: 0 0; - border: 0; - color: #666; - color: var(--clr-tree-link-text-color, #666); - cursor: pointer; - line-height: inherit; - text-align: left; + display: inline-block; + height: 100%; + width: 100%; + margin: 0; + padding: 0 0 0 0.3rem; + background: 0 0; + border: 0; + color: #666; + color: var(--clr-tree-link-text-color, #666); + cursor: pointer; + line-height: inherit; + text-align: left; } .clr-treenode-link:active, .clr-treenode-link:hover, .clr-treenode-link:link, .clr-treenode-link:visited { - color: inherit; + color: inherit; } .clr-treenode-link:focus, .clr-treenode-link:hover { - background: #e8e8e8; - background: var(--clr-tree-link-hover-color, #e8e8e8); - text-decoration: none; + background: #e8e8e8; + background: var(--clr-tree-link-hover-color, #e8e8e8); + text-decoration: none; } .clr-treenode-link:focus { - outline: 0; + outline: 0; } .clr-treenode-link.active { - background: #d8e3e9; - background: var(--clr-tree-link-selection-color, #d8e3e9); - color: #000; - color: var(--clr-tree-node-caret-link-hover-color, #000); + background: #d8e3e9; + background: var(--clr-tree-link-selection-color, #d8e3e9); + color: #000; + color: var(--clr-tree-node-caret-link-hover-color, #000); } .clr-tree-node-content-container > .clr-checkbox-wrapper { - height: 1.62rem; - width: 1.62rem; - padding-top: 0.21rem; - padding-left: 0.41rem; + height: 1.62rem; + width: 1.62rem; + padding-top: 0.21rem; + padding-left: 0.41rem; } .clr-tree-node-content-container > .clr-checkbox-wrapper:first-child { - margin-left: 1.62rem; + margin-left: 1.62rem; } .clr-treenode-content .label { - margin-left: 0.3rem; + margin-left: 0.3rem; } @supports (-ms-ime-align: auto) { - .clr-treenode-content .label { - margin-left: 0.15rem; - } + .clr-treenode-content .label { + margin-left: 0.15rem; + } } _:-ms-input-placeholder .clr-treenode-content .label, :root .clr-treenode-content .label { - margin-left: 0.15rem; + margin-left: 0.15rem; } :root { - --clr-datagrid-font-color: #acbac3; - --clr-datagrid-default-border-color: var(--clr-color-neutral-400); - --clr-datagrid-icon-color: var(--clr-color-neutral-600); - --clr-datagrid-row-hover: var(--clr-color-neutral-200); - --clr-datagrid-row-hover-font-color: var(--clr-color-neutral-1000); - --clr-datagrid-action-toggle: var(--clr-color-neutral-700); - --clr-datagrid-pagination-btn-color: var(--clr-color-neutral-700); - --clr-datagrid-pagination-btn-disabled-color: var(--clr-color-neutral-600); - --clr-datagrid-pagination-input-border-color: var( - --clr-datagrid-default-border-color - ); - --clr-datagrid-pagination-input-border-focus-color: var( - --clr-color-action-400 - ); - --clr-datagrid-popover-bg-color: var(--clr-color-neutral-0); - --clr-datagrid-popover-border-color: var( - --clr-datagrid-default-border-color - ); - --clr-datagrid-action-popover-hover-color: var(--clr-color-neutral-200); - --clr-datagrid-row-selected: var(--clr-color-neutral-1000); - --clr-datagrid-loading-background: rgba(255, 255, 255, 0.6); - --clr-datagrid-popovers-box-shadow-color: rgba(140, 140, 140, 0.25); - --clr-datagrid-column-switch-header-font-color: var( - --clr-color-neutral-500 - ); - --clr-datagrid-column-switch-header-font-hover-color: var( - --clr-color-action-600 - ); - --clr-datagrid-detail-caret-icon-open-bg-color: var(--clr-color-action-600); - --clr-datagrid-detail-caret-icon-open-icon-color: var( - --clr-color-neutral-0 - ); - --clr-datagrid-placeholder-color: var(--clr-color-neutral-500); - --clr-datagrid-placeholder-font-size: 0.8rem; - --clr-datagrid-pagination-btn-disabled-opacity: 0.56; + --clr-datagrid-font-color: #acbac3; + --clr-datagrid-default-border-color: var(--clr-color-neutral-400); + --clr-datagrid-icon-color: var(--clr-color-neutral-600); + --clr-datagrid-row-hover: var(--clr-color-neutral-200); + --clr-datagrid-row-hover-font-color: var(--clr-color-neutral-1000); + --clr-datagrid-action-toggle: var(--clr-color-neutral-700); + --clr-datagrid-pagination-btn-color: var(--clr-color-neutral-700); + --clr-datagrid-pagination-btn-disabled-color: var(--clr-color-neutral-600); + --clr-datagrid-pagination-input-border-color: var( + --clr-datagrid-default-border-color + ); + --clr-datagrid-pagination-input-border-focus-color: var( + --clr-color-action-400 + ); + --clr-datagrid-popover-bg-color: var(--clr-color-neutral-0); + --clr-datagrid-popover-border-color: var(--clr-datagrid-default-border-color); + --clr-datagrid-action-popover-hover-color: var(--clr-color-neutral-200); + --clr-datagrid-row-selected: var(--clr-color-neutral-1000); + --clr-datagrid-loading-background: rgba(255, 255, 255, 0.6); + --clr-datagrid-popovers-box-shadow-color: rgba(140, 140, 140, 0.25); + --clr-datagrid-column-switch-header-font-color: var(--clr-color-neutral-500); + --clr-datagrid-column-switch-header-font-hover-color: var( + --clr-color-action-600 + ); + --clr-datagrid-detail-caret-icon-open-bg-color: var(--clr-color-action-600); + --clr-datagrid-detail-caret-icon-open-icon-color: var(--clr-color-neutral-0); + --clr-datagrid-placeholder-color: var(--clr-color-neutral-500); + --clr-datagrid-placeholder-font-size: 0.8rem; + --clr-datagrid-pagination-btn-disabled-opacity: 0.56; } .datagrid { - border-collapse: separate; - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-table-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-table-border-color, #ccc); - border-radius: 0.15rem; - border-radius: var(--clr-table-border-radius, 0.15rem); - background-color: #fff; - background-color: var(--clr-table-bgcolor, #fff); - color: #666; - color: var(--clr-table-font-color, #666); - margin: 0; - margin-top: 1.2rem; - max-width: 100%; - width: 100%; + border-collapse: separate; + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-table-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-table-border-color, #ccc); + border-radius: 0.15rem; + border-radius: var(--clr-table-border-radius, 0.15rem); + background-color: #fff; + background-color: var(--clr-table-bgcolor, #fff); + color: #666; + color: var(--clr-table-font-color, #666); + margin: 0; + margin-top: 1.2rem; + max-width: 100%; + width: 100%; } .datagrid .datagrid-cell, .datagrid .datagrid-column { - font-size: 0.65rem; - line-height: 0.7rem; - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var(--clr-table-borderwidth, 0.05rem); - border-top-color: #e8e8e8; - border-top-color: var(--clr-tablerow-bordercolor, #e8e8e8); - padding: 0.55rem 0.6rem 0.55rem; - text-align: center; - vertical-align: top; + font-size: 0.65rem; + line-height: 0.7rem; + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var(--clr-table-borderwidth, 0.05rem); + border-top-color: #e8e8e8; + border-top-color: var(--clr-tablerow-bordercolor, #e8e8e8); + padding: 0.55rem 0.6rem 0.55rem; + text-align: center; + vertical-align: top; } .datagrid .datagrid-cell.left, .datagrid .datagrid-column.left { - text-align: left; + text-align: left; } .datagrid .datagrid-cell.left:first-child, .datagrid .datagrid-column.left:first-child { - padding-left: 0.3rem; + padding-left: 0.3rem; } .datagrid .datagrid-column { - color: #666; - color: var(--clr-thead-color, #666); - font-size: 0.55rem; - font-weight: 600; - letter-spacing: 0.03em; - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); - vertical-align: bottom; - border-bottom-style: solid; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-table-borderwidth, 0.05rem); - border-bottom-color: #ccc; - border-bottom-color: var(--clr-table-border-color, #ccc); - border-top: 0 none; + color: #666; + color: var(--clr-thead-color, #666); + font-size: 0.55rem; + font-weight: 600; + letter-spacing: 0.03em; + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); + vertical-align: bottom; + border-bottom-style: solid; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-table-borderwidth, 0.05rem); + border-bottom-color: #ccc; + border-bottom-color: var(--clr-table-border-color, #ccc); + border-top: 0 none; } .datagrid .datagrid-body .datagrid-row:first-child .datagrid-cell { - border-top: 0 none; + border-top: 0 none; } .datagrid .datagrid-body + .datagrid-body { - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var(--clr-table-borderwidth, 0.05rem); - border-top-color: #ccc; - border-top-color: var(--clr-table-border-color, #ccc); + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var(--clr-table-borderwidth, 0.05rem); + border-top-color: #ccc; + border-top-color: var(--clr-table-border-color, #ccc); } .datagrid .datagrid-header .datagrid-column:first-child { - border-radius: 0; - border-top-left-radius: 0.1rem; - border-top-left-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-top-left-radius: 0.1rem; + border-top-left-radius: var(--clr-table-cornercellradius, 0.1rem); } .datagrid .datagrid-header .datagrid-column:last-child { - border-radius: 0; - border-top-right-radius: 0.1rem; - border-top-right-radius: var(--clr-table-cornercellradius, 0.1rem); + border-radius: 0; + border-top-right-radius: 0.1rem; + border-top-right-radius: var(--clr-table-cornercellradius, 0.1rem); } .datagrid - .datagrid-body:last-child - .datagrid-row:last-child - .datagrid-cell:first-child { - border-radius: 0; - border-bottom-left-radius: 0.1rem; - border-bottom-left-radius: var(--clr-table-cornercellradius, 0.1rem); + .datagrid-body:last-child + .datagrid-row:last-child + .datagrid-cell:first-child { + border-radius: 0; + border-bottom-left-radius: 0.1rem; + border-bottom-left-radius: var(--clr-table-cornercellradius, 0.1rem); } .datagrid - .datagrid-body:last-child - .datagrid-row:last-child - .datagrid-cell:last-child { - border-radius: 0; - border-bottom-right-radius: 0.1rem; - border-bottom-right-radius: var(--clr-table-cornercellradius, 0.1rem); + .datagrid-body:last-child + .datagrid-row:last-child + .datagrid-cell:last-child { + border-radius: 0; + border-bottom-right-radius: 0.1rem; + border-bottom-right-radius: var(--clr-table-cornercellradius, 0.1rem); } .datagrid-compact .datagrid-cell, .datagrid-compact .datagrid-column { - padding-top: 0.3rem; - padding-bottom: 0.25rem; + padding-top: 0.3rem; + padding-bottom: 0.25rem; } .datagrid-host { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-flow: column nowrap; - flex-flow: column nowrap; - position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + position: relative; } .datagrid { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - -ms-overflow-style: -ms-autohiding-scrollbar; - overflow: auto; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - margin-top: 0.6rem; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + -ms-overflow-style: -ms-autohiding-scrollbar; + overflow: auto; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + margin-top: 0.6rem; } .datagrid-container { - width: auto; - font-size: 0.65rem; - padding: 0.55rem 0.6rem 0.55rem; + width: auto; + font-size: 0.65rem; + padding: 0.55rem 0.6rem 0.55rem; } .datagrid-expandable-caret { - padding: 0.1rem 0.2rem 0.15rem; - text-align: center; + padding: 0.1rem 0.2rem 0.15rem; + text-align: center; } .datagrid-expandable-caret .datagrid-expandable-caret-button { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - cursor: pointer; - height: 1.5rem; - width: 1.5rem; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + cursor: pointer; + height: 1.5rem; + width: 1.5rem; } button.datagrid-expandable-caret .datagrid-expandable-caret-button { - cursor: pointer; + cursor: pointer; } .datagrid-expandable-caret .datagrid-expandable-caret-icon { - color: #8c8c8c; - color: var(--clr-datagrid-icon-color, #8c8c8c); - margin-top: 0.15rem; + color: #8c8c8c; + color: var(--clr-datagrid-icon-color, #8c8c8c); + margin-top: 0.15rem; } .datagrid-expandable-caret .datagrid-expandable-caret-icon svg { - -webkit-transition: -webkit-transform 0.2s ease-in-out; - transition: -webkit-transform 0.2s ease-in-out; - transition: transform 0.2s ease-in-out; - transition: - transform 0.2s ease-in-out, - -webkit-transform 0.2s ease-in-out; + -webkit-transition: -webkit-transform 0.2s ease-in-out; + transition: -webkit-transform 0.2s ease-in-out; + transition: transform 0.2s ease-in-out; + transition: + transform 0.2s ease-in-out, + -webkit-transform 0.2s ease-in-out; } .datagrid-expandable-caret .spinner { - margin-top: 0.3rem; + margin-top: 0.3rem; } .datagrid-expandable-caret.datagrid-column { - padding: 0.55rem 0.6rem 0.55rem; + padding: 0.55rem 0.6rem 0.55rem; } .datagrid-body, .datagrid-cell, @@ -14193,3514 +14117,3513 @@ button.datagrid-expandable-caret .datagrid-expandable-caret-button { .datagrid-fixed-column, .datagrid-header, .datagrid-row { - display: block; + display: block; } .datagrid-row { - border-top-style: solid; - border-top-width: 0.05rem; - border-top-width: var(--clr-table-borderwidth, 0.05rem); - border-top-color: #ccc; - border-top-color: var(--clr-table-border-color, #ccc); + border-top-style: solid; + border-top-width: 0.05rem; + border-top-width: var(--clr-table-borderwidth, 0.05rem); + border-top-color: #ccc; + border-top-color: var(--clr-table-border-color, #ccc); } .datagrid-row:first-of-type { - border-top: none; + border-top: none; } .datagrid-row:hover { - background-color: #e8e8e8; - background-color: var(--clr-datagrid-row-hover, #e8e8e8); + background-color: #e8e8e8; + background-color: var(--clr-datagrid-row-hover, #e8e8e8); } .datagrid-row:hover .datagrid-row-sticky { - background-color: #e8e8e8; - background-color: var(--clr-datagrid-row-hover, #e8e8e8); + background-color: #e8e8e8; + background-color: var(--clr-datagrid-row-hover, #e8e8e8); } .datagrid-row.datagrid-selected { - color: #000; - color: var(--clr-datagrid-row-selected, #000); - background-color: #d8e3e9; - background-color: var(--clr-global-selection-color, #d8e3e9); + color: #000; + color: var(--clr-datagrid-row-selected, #000); + background-color: #d8e3e9; + background-color: var(--clr-global-selection-color, #d8e3e9); } .datagrid-row.datagrid-selected .datagrid-row-sticky { - color: #000; - color: var(--clr-datagrid-row-selected, #000); - background-color: #d8e3e9; - background-color: var(--clr-global-selection-color, #d8e3e9); + color: #000; + color: var(--clr-datagrid-row-selected, #000); + background-color: #d8e3e9; + background-color: var(--clr-global-selection-color, #d8e3e9); } .datagrid-row .datagrid-action-overflow { - position: absolute; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.3rem; - margin-left: 0.3rem; - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; - white-space: nowrap; + position: absolute; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.3rem; + margin-left: 0.3rem; + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; + white-space: nowrap; } .datagrid-row .datagrid-action-overflow::before { - content: ''; - position: absolute; - top: 50%; - right: 100%; - height: 0; - width: 0; - margin-top: -0.3rem; - border-top: 0.3rem solid transparent; - border-bottom: 0.3rem solid transparent; - border-right-width: 0.3rem; - border-right-style: solid; - border-right-color: #ccc; - border-right-color: var(--clr-datagrid-popover-border-color, #ccc); + content: ""; + position: absolute; + top: 50%; + right: 100%; + height: 0; + width: 0; + margin-top: -0.3rem; + border-top: 0.3rem solid transparent; + border-bottom: 0.3rem solid transparent; + border-right-width: 0.3rem; + border-right-style: solid; + border-right-color: #ccc; + border-right-color: var(--clr-datagrid-popover-border-color, #ccc); } .datagrid-row .datagrid-action-overflow::after { - content: ''; - position: absolute; - top: 50%; - right: 100%; - height: 0; - width: 0; - margin-top: -0.25rem; - border-top: 0.25rem solid transparent; - border-bottom: 0.25rem solid transparent; - border-right-width: 0.25rem; - border-right-style: solid; - border-right-color: #fff; - border-right-color: var(--clr-datagrid-popover-bg-color, #fff); + content: ""; + position: absolute; + top: 50%; + right: 100%; + height: 0; + width: 0; + margin-top: -0.25rem; + border-top: 0.25rem solid transparent; + border-bottom: 0.25rem solid transparent; + border-right-width: 0.25rem; + border-right-style: solid; + border-right-color: #fff; + border-right-color: var(--clr-datagrid-popover-bg-color, #fff); } .datagrid-row .datagrid-action-overflow .action-item { - color: #666; - color: var(--clr-dropdown-text-color, #666); - font-size: 0.7rem; - letter-spacing: normal; - background: 0 0; - border: 0; - cursor: pointer; - display: block; - line-height: 1.15rem; - margin: 0; - padding: 0.05rem 1.2rem 0; - text-align: left; - width: 100%; + color: #666; + color: var(--clr-dropdown-text-color, #666); + font-size: 0.7rem; + letter-spacing: normal; + background: 0 0; + border: 0; + cursor: pointer; + display: block; + line-height: 1.15rem; + margin: 0; + padding: 0.05rem 1.2rem 0; + text-align: left; + width: 100%; } .datagrid-row .datagrid-action-overflow .action-item:focus, .datagrid-row .datagrid-action-overflow .action-item:hover { - background-color: #e8e8e8; - background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); - text-decoration: none; + background-color: #e8e8e8; + background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); + text-decoration: none; } .datagrid-row .datagrid-action-overflow .action-item.active { - background: #e8e8e8; - background: var(--clr-datagrid-row-hover, #e8e8e8); - color: #000; - color: var(--clr-color-neutral-1000, #000); + background: #e8e8e8; + background: var(--clr-datagrid-row-hover, #e8e8e8); + color: #000; + color: var(--clr-color-neutral-1000, #000); } .datagrid-row .datagrid-action-overflow .action-item:focus { - outline: 0; + outline: 0; } .datagrid-row .datagrid-action-overflow .action-item.disabled, .datagrid-row .datagrid-action-overflow .action-item:disabled { - cursor: not-allowed; - opacity: 0.4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: not-allowed; + opacity: 0.4; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .datagrid-row .datagrid-action-overflow .action-item.disabled:hover, .datagrid-row .datagrid-action-overflow .action-item:disabled:hover { - background: 0 0; + background: 0 0; } .datagrid-row .datagrid-action-overflow .action-item.disabled:active, .datagrid-row .datagrid-action-overflow .action-item.disabled:focus, .datagrid-row .datagrid-action-overflow .action-item:disabled:active, .datagrid-row .datagrid-action-overflow .action-item:disabled:focus { - background: 0 0; - -webkit-box-shadow: none; - box-shadow: none; + background: 0 0; + -webkit-box-shadow: none; + box-shadow: none; } .datagrid-row .datagrid-action-overflow .action-item cds-icon { - vertical-align: middle; - -webkit-transform: translate3d(0, -0.05rem, 0); - transform: translate3d(0, -0.05rem, 0); + vertical-align: middle; + -webkit-transform: translate3d(0, -0.05rem, 0); + transform: translate3d(0, -0.05rem, 0); } .datagrid-row .datagrid-action-overflow { - position: absolute; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.3rem; - margin-left: 0.3rem; - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; - white-space: nowrap; + position: absolute; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.3rem; + margin-left: 0.3rem; + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; + white-space: nowrap; } .datagrid-row .datagrid-action-overflow::before { - content: ''; - position: absolute; - top: 50%; - right: 100%; - width: 0; - height: 0; - margin-top: -0.3rem; - border-top: 0.3rem solid transparent; - border-bottom: 0.3rem solid transparent; - border-right-width: 0.3rem; - border-right-style: solid; - border-right-color: #ccc; - border-right-color: var(--clr-datagrid-popover-border-color, #ccc); + content: ""; + position: absolute; + top: 50%; + right: 100%; + width: 0; + height: 0; + margin-top: -0.3rem; + border-top: 0.3rem solid transparent; + border-bottom: 0.3rem solid transparent; + border-right-width: 0.3rem; + border-right-style: solid; + border-right-color: #ccc; + border-right-color: var(--clr-datagrid-popover-border-color, #ccc); } .datagrid-row .datagrid-action-overflow::after { - content: ''; - position: absolute; - top: 50%; - right: 100%; - width: 0; - height: 0; - margin-top: -0.25rem; - border-top: 0.25rem solid transparent; - border-bottom: 0.25rem solid transparent; - border-right-width: 0.25rem; - border-right-style: solid; - border-right-color: #fff; - border-right-color: var(--clr-datagrid-popover-bg-color, #fff); + content: ""; + position: absolute; + top: 50%; + right: 100%; + width: 0; + height: 0; + margin-top: -0.25rem; + border-top: 0.25rem solid transparent; + border-bottom: 0.25rem solid transparent; + border-right-width: 0.25rem; + border-right-style: solid; + border-right-color: #fff; + border-right-color: var(--clr-datagrid-popover-bg-color, #fff); } .datagrid-row .datagrid-action-overflow .action-item { - color: #666; - color: var(--clr-dropdown-text-color, #666); - font-size: 0.7rem; - letter-spacing: normal; - background: 0 0; - border: 0; - cursor: pointer; - display: block; - line-height: 1.15rem; - margin: 0; - padding: 0.05rem 1.2rem 0; - text-align: left; - width: 100%; + color: #666; + color: var(--clr-dropdown-text-color, #666); + font-size: 0.7rem; + letter-spacing: normal; + background: 0 0; + border: 0; + cursor: pointer; + display: block; + line-height: 1.15rem; + margin: 0; + padding: 0.05rem 1.2rem 0; + text-align: left; + width: 100%; } .datagrid-row .datagrid-action-overflow .action-item:focus, .datagrid-row .datagrid-action-overflow .action-item:hover { - background-color: #e8e8e8; - background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); - text-decoration: none; + background-color: #e8e8e8; + background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); + text-decoration: none; } .datagrid-row .datagrid-action-overflow .action-item.active { - background: #e8e8e8; - background: var(--clr-datagrid-row-hover, #e8e8e8); - color: #000; - color: var(--clr-color-neutral-1000, #000); + background: #e8e8e8; + background: var(--clr-datagrid-row-hover, #e8e8e8); + color: #000; + color: var(--clr-color-neutral-1000, #000); } .datagrid-row .datagrid-action-overflow .action-item:focus { - outline: 0; + outline: 0; } .datagrid-row .datagrid-action-overflow .action-item.disabled, .datagrid-row .datagrid-action-overflow .action-item:disabled { - cursor: not-allowed; - opacity: 0.4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: not-allowed; + opacity: 0.4; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .datagrid-row .datagrid-action-overflow .action-item.disabled:hover, .datagrid-row .datagrid-action-overflow .action-item:disabled:hover { - background: 0 0; + background: 0 0; } .datagrid-row .datagrid-action-overflow .action-item.disabled:active, .datagrid-row .datagrid-action-overflow .action-item.disabled:focus, .datagrid-row .datagrid-action-overflow .action-item:disabled:active, .datagrid-row .datagrid-action-overflow .action-item:disabled:focus { - background: 0 0; - -webkit-box-shadow: none; - box-shadow: none; + background: 0 0; + -webkit-box-shadow: none; + box-shadow: none; } .datagrid-row .datagrid-action-overflow .action-item cds-icon { - vertical-align: middle; - -webkit-transform: translate3d(0, -0.05rem, 0); - transform: translate3d(0, -0.05rem, 0); + vertical-align: middle; + -webkit-transform: translate3d(0, -0.05rem, 0); + transform: translate3d(0, -0.05rem, 0); } .datagrid-row .datagrid-action-overflow { - position: absolute; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.3rem; - margin-left: 0.3rem; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-color: 0.15rem; - border-color: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; - white-space: nowrap; + position: absolute; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.3rem; + margin-left: 0.3rem; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-color: 0.15rem; + border-color: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; + white-space: nowrap; } .datagrid-row .datagrid-action-overflow::before { - content: ''; - position: absolute; - top: 50%; - right: 100%; - width: 0; - height: 0; - margin-top: -0.3rem; - border-right: 0.3rem solid; - border-right-color: #ccc; - border-right-color: var(--clr-datagrid-popover-border-color, #ccc); - border-top: 0.3rem solid transparent; - border-bottom: 0.3rem solid transparent; + content: ""; + position: absolute; + top: 50%; + right: 100%; + width: 0; + height: 0; + margin-top: -0.3rem; + border-right: 0.3rem solid; + border-right-color: #ccc; + border-right-color: var(--clr-datagrid-popover-border-color, #ccc); + border-top: 0.3rem solid transparent; + border-bottom: 0.3rem solid transparent; } .datagrid-row .datagrid-action-overflow::after { - content: ''; - position: absolute; - top: 50%; - right: 100%; - width: 0; - height: 0; - margin-top: -0.25rem; - border-right: 0.25rem solid #fff; - border-top: 0.25rem solid transparent; - border-bottom: 0.25rem solid transparent; + content: ""; + position: absolute; + top: 50%; + right: 100%; + width: 0; + height: 0; + margin-top: -0.25rem; + border-right: 0.25rem solid #fff; + border-top: 0.25rem solid transparent; + border-bottom: 0.25rem solid transparent; } .datagrid-row .datagrid-action-overflow .action-item { - color: #666; - color: var(--clr-dropdown-text-color, #666); - font-size: 0.7rem; - letter-spacing: normal; - background: 0 0; - border: 0; - cursor: pointer; - display: block; - line-height: 1.15rem; - margin: 0; - padding: 0.05rem 1.2rem 0; - text-align: left; - width: 100%; + color: #666; + color: var(--clr-dropdown-text-color, #666); + font-size: 0.7rem; + letter-spacing: normal; + background: 0 0; + border: 0; + cursor: pointer; + display: block; + line-height: 1.15rem; + margin: 0; + padding: 0.05rem 1.2rem 0; + text-align: left; + width: 100%; } .datagrid-row .datagrid-action-overflow .action-item:focus, .datagrid-row .datagrid-action-overflow .action-item:hover { - text-decoration: none; - background-color: #e8e8e8; - background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); + text-decoration: none; + background-color: #e8e8e8; + background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); } .datagrid-row .datagrid-action-overflow .action-item.active { - background: #e8e8e8; - background: var(--clr-datagrid-row-hover, #e8e8e8); - color: #000; - color: var(--clr-datagrid-row-hover-font-color, #000); + background: #e8e8e8; + background: var(--clr-datagrid-row-hover, #e8e8e8); + color: #000; + color: var(--clr-datagrid-row-hover-font-color, #000); } .datagrid-row .datagrid-action-overflow .action-item:focus { - outline: 0; + outline: 0; } .datagrid-row .datagrid-action-overflow .action-item.disabled, .datagrid-row .datagrid-action-overflow .action-item:disabled { - cursor: not-allowed; - opacity: 0.4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: not-allowed; + opacity: 0.4; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .datagrid-row .datagrid-action-overflow .action-item.disabled:hover, .datagrid-row .datagrid-action-overflow .action-item:disabled:hover { - background: 0 0; + background: 0 0; } .datagrid-row .datagrid-action-overflow .action-item.disabled:active, .datagrid-row .datagrid-action-overflow .action-item.disabled:focus, .datagrid-row .datagrid-action-overflow .action-item:disabled:active, .datagrid-row .datagrid-action-overflow .action-item:disabled:focus { - background: 0 0; - -webkit-box-shadow: none; - box-shadow: none; + background: 0 0; + -webkit-box-shadow: none; + box-shadow: none; } .datagrid-row .datagrid-action-overflow .action-item cds-icon { - vertical-align: middle; - -webkit-transform: translate3d(0, -0.05rem, 0); - transform: translate3d(0, -0.05rem, 0); + vertical-align: middle; + -webkit-transform: translate3d(0, -0.05rem, 0); + transform: translate3d(0, -0.05rem, 0); } .datagrid-row .datagrid-row-detail-wrapper { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .datagrid-row .datagrid-row-detail { - width: auto; + width: auto; } .datagrid-row .datagrid-row-detail .datagrid-cell { - padding-top: 0; - border: none; + padding-top: 0; + border: none; } .datagrid-row .datagrid-select { - min-width: 0.4rem; + min-width: 0.4rem; } .datagrid-row .datagrid-signpost-trigger .signpost { - margin: -0.36rem 0; - height: 1.236rem; + margin: -0.36rem 0; + height: 1.236rem; } .datagrid-row .datagrid-signpost-trigger .signpost .signpost-trigger { - height: inherit; - line-height: 1.2rem; + height: inherit; + line-height: 1.2rem; } .datagrid-row .datagrid-row-sticky { - z-index: 500; + z-index: 500; } .datagrid-row-sticky { - background-color: #fff; - background-color: var(--clr-table-bgcolor, #fff); - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: nowrap; - flex-wrap: nowrap; - position: sticky; - left: 0; - z-index: 502; + background-color: #fff; + background-color: var(--clr-table-bgcolor, #fff); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + position: sticky; + left: 0; + z-index: 502; } .datagrid-row-sticky .datagrid-cell:last-child:after { - content: ''; - width: 0.05rem; - height: calc(100% - 0.5rem); - position: absolute; - right: 0; - top: 0.25rem; - background-color: #ccc; - background-color: var(--clr-table-border-color, #ccc); + content: ""; + width: 0.05rem; + height: calc(100% - 0.5rem); + position: absolute; + right: 0; + top: 0.25rem; + background-color: #ccc; + background-color: var(--clr-table-border-color, #ccc); } .datagrid-row-scrollable { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-flow: column nowrap; - flex-flow: column nowrap; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; } .datagrid-row-scrollable.is-replaced { - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } .datagrid-row-scrollable.is-replaced .datagrid-scrolling-cells { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; } .datagrid-row-scrollable - .datagrid-column:last-child - .datagrid-column-separator { - display: none; + .datagrid-column:last-child + .datagrid-column-separator { + display: none; } .datagrid-row-flex { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row nowrap; - flex-flow: row nowrap; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; } .datagrid-row-flex .datagrid-row-detail { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row nowrap; - flex-flow: row nowrap; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; } .datagrid-row-flex .datagrid-row-detail .datagrid-cell { - padding-top: 0; + padding-top: 0; } .datagrid-scrolling-cells, .datagrid-scrolling-details { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row nowrap; - flex-flow: row nowrap; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; } .datagrid-action-bar { - margin-top: 1.2rem; + margin-top: 1.2rem; } .datagrid-action-bar ~ .datagrid-spinner { - height: calc(100% - 1.2rem); - top: 1.2rem; + height: calc(100% - 1.2rem); + top: 1.2rem; } .datagrid-header { - position: sticky; - top: 0; - z-index: 501; - width: auto; + position: sticky; + top: 0; + z-index: 501; + width: auto; } .datagrid-header .datagrid-column { - border-bottom: none; + border-bottom: none; } .datagrid-header .datagrid-row { - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); - border-top: none; - border-bottom-style: solid; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-global-borderwidth, 0.05rem); - border-bottom-color: #ccc; - border-bottom-color: var(--clr-table-border-color, #ccc); + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); + border-top: none; + border-bottom-style: solid; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-global-borderwidth, 0.05rem); + border-bottom-color: #ccc; + border-bottom-color: var(--clr-table-border-color, #ccc); } .datagrid-header .datagrid-row .datagrid-row-sticky { - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); } .datagrid-header .datagrid-row:hover { - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); } .datagrid-header .datagrid-row:hover .datagrid-row-sticky { - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); } .datagrid-header .datagrid-row-scrollable { - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } .datagrid-table-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-height: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + min-height: 100%; } .datagrid-table { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - -ms-flex-line-pack: start; - align-content: flex-start; - position: relative; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -ms-flex-line-pack: start; + align-content: flex-start; + position: relative; } .datagrid-table .datagrid-body { - width: auto; + width: auto; } .datagrid-table .datagrid-column { - text-align: left; - min-width: 4.8rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - vertical-align: top; - border: none; -} -.datagrid-table .datagrid-column clr-dg-filter, -.datagrid-table .datagrid-column clr-dg-numeric-filter, -.datagrid-table .datagrid-column clr-dg-string-filter { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-ordinal-group: 100; - -ms-flex-order: 99; - order: 99; - margin-left: auto; -} -.datagrid-table .datagrid-column .datagrid-filter-input-spacer { - width: 0.6rem; - display: inline-block; -} -.datagrid-table .datagrid-column .datagrid-numeric-filter-input { - width: 3.9rem; -} -.datagrid-table .datagrid-column .datagrid-filter-toggle { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - cursor: pointer; - float: right; - vertical-align: middle; - height: 0.7rem; - width: 0.7rem; - margin-left: 0.3rem; - background-repeat: no-repeat; - background-size: contain; + text-align: left; + min-width: 4.8rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + vertical-align: top; + border: none; +} +.datagrid-table .datagrid-column clr-dg-filter, +.datagrid-table .datagrid-column clr-dg-numeric-filter, +.datagrid-table .datagrid-column clr-dg-string-filter { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-ordinal-group: 100; + -ms-flex-order: 99; + order: 99; + margin-left: auto; +} +.datagrid-table .datagrid-column .datagrid-filter-input-spacer { + width: 0.6rem; + display: inline-block; +} +.datagrid-table .datagrid-column .datagrid-numeric-filter-input { + width: 3.9rem; +} +.datagrid-table .datagrid-column .datagrid-filter-toggle { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + cursor: pointer; + float: right; + vertical-align: middle; + height: 0.7rem; + width: 0.7rem; + margin-left: 0.3rem; + background-repeat: no-repeat; + background-size: contain; } button.datagrid-table .datagrid-column .datagrid-filter-toggle { - cursor: pointer; + cursor: pointer; } .datagrid-table .datagrid-column .datagrid-filter-toggle cds-icon { - color: #b3b3b3; - color: var(--clr-color-neutral-500, #b3b3b3); + color: #b3b3b3; + color: var(--clr-color-neutral-500, #b3b3b3); } .datagrid-table .datagrid-column .datagrid-filter-toggle:hover cds-icon { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); } .datagrid-table - .datagrid-column - .datagrid-filter-toggle.datagrid-filter-open - cds-icon { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); + .datagrid-column + .datagrid-filter-toggle.datagrid-filter-open + cds-icon { + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); } .datagrid-table - .datagrid-column - .datagrid-filter-toggle.datagrid-filtered - cds-icon { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); + .datagrid-column + .datagrid-filter-toggle.datagrid-filtered + cds-icon { + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); } .datagrid-table .datagrid-column .datagrid-filter { - top: 100%; - right: 0; - margin-top: 0.24rem; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.9rem; - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; + top: 100%; + right: 0; + margin-top: 0.24rem; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.9rem; + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; } .datagrid-table - .datagrid-column - .datagrid-filter - .datagrid-filter-close-wrapper { - text-align: right; + .datagrid-column + .datagrid-filter + .datagrid-filter-close-wrapper { + text-align: right; } .datagrid-table - .datagrid-column - .datagrid-filter - .datagrid-filter-close-wrapper - .close { - float: none; + .datagrid-column + .datagrid-filter + .datagrid-filter-close-wrapper + .close { + float: none; } .datagrid-table .datagrid-column .datagrid-filter .datagrid-filter-apply { - margin-bottom: 0; + margin-bottom: 0; } .datagrid-table .datagrid-column .datagrid-filter { - top: 100%; - right: 0; - margin-top: 0.24rem; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.9rem; - border-style: solid; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; + top: 100%; + right: 0; + margin-top: 0.24rem; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.9rem; + border-style: solid; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; } .datagrid-table - .datagrid-column - .datagrid-filter - .datagrid-filter-close-wrapper { - text-align: right; + .datagrid-column + .datagrid-filter + .datagrid-filter-close-wrapper { + text-align: right; } .datagrid-table - .datagrid-column - .datagrid-filter - .datagrid-filter-close-wrapper - .close { - float: none; + .datagrid-column + .datagrid-filter + .datagrid-filter-close-wrapper + .close { + float: none; } .datagrid-table .datagrid-column .datagrid-filter .datagrid-filter-apply { - margin-bottom: 0; + margin-bottom: 0; } .datagrid-table .datagrid-column .datagrid-filter { - top: 100%; - right: 0; - margin-top: 0.24rem; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.9rem; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - border-style: solid; - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; + top: 100%; + right: 0; + margin-top: 0.24rem; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.9rem; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + border-style: solid; + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; } .datagrid-table - .datagrid-column - .datagrid-filter - .datagrid-filter-close-wrapper { - text-align: right; + .datagrid-column + .datagrid-filter + .datagrid-filter-close-wrapper { + text-align: right; } .datagrid-table - .datagrid-column - .datagrid-filter - .datagrid-filter-close-wrapper - .close { - float: none; + .datagrid-column + .datagrid-filter + .datagrid-filter-close-wrapper + .close { + float: none; } .datagrid-table .datagrid-column .datagrid-filter .datagrid-filter-apply { - margin-bottom: 0; + margin-bottom: 0; } .datagrid-table .datagrid-column.datagrid-fixed-width { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; } .datagrid-table .datagrid-column .datagrid-column-flex { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; } .datagrid-table .datagrid-column .datagrid-column-title { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - color: #666; - color: var(--clr-table-font-color, #666); - text-align: left; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -ms-flex-item-align: center; - align-self: center; - display: -webkit-box; - display: -ms-flexbox; - display: flex; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + color: #666; + color: var(--clr-table-font-color, #666); + text-align: left; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-item-align: center; + align-self: center; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } button.datagrid-table .datagrid-column .datagrid-column-title { - cursor: pointer; + cursor: pointer; } .datagrid-table - .datagrid-column - .datagrid-column-title - .signpost - .signpost-action.btn { - height: inherit; - line-height: inherit; + .datagrid-column + .datagrid-column-title + .signpost + .signpost-action.btn { + height: inherit; + line-height: inherit; } .datagrid-table - .datagrid-column - .datagrid-column-title - .clr-checkbox-wrapper - .clr-control-label { - margin-top: -0.48rem; + .datagrid-column + .datagrid-column-title + .clr-checkbox-wrapper + .clr-control-label { + margin-top: -0.48rem; } .datagrid-table .datagrid-column button.datagrid-column-title:hover { - text-decoration: underline; - cursor: pointer; + text-decoration: underline; + cursor: pointer; } .datagrid-table .datagrid-column button.datagrid-column-title .sort-icon { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); - margin-left: auto; - height: 0.7rem; - width: 0.7rem; - vertical-align: middle; + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); + margin-left: auto; + height: 0.7rem; + width: 0.7rem; + vertical-align: middle; } .datagrid-table .datagrid-column .datagrid-column-separator { - position: relative; - left: 0.6rem; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: 0.05rem; - width: var(--clr-global-borderwidth, 0.05rem); - -webkit-box-ordinal-group: 101; - -ms-flex-order: 100; - order: 100; - margin-left: auto; - height: 100%; + position: relative; + left: 0.6rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 0.05rem; + width: var(--clr-global-borderwidth, 0.05rem); + -webkit-box-ordinal-group: 101; + -ms-flex-order: 100; + order: 100; + margin-left: auto; + height: 100%; } .datagrid-table .datagrid-column .datagrid-column-separator::after { - content: ''; - position: absolute; - height: calc(100% + 0.6rem - 0.05rem); - width: 0.05rem; - top: calc(-0.5 * 0.6rem + 0.05rem); - left: 0; - background-color: #ccc; - background-color: var(--clr-table-border-color, #ccc); + content: ""; + position: absolute; + height: calc(100% + 0.6rem - 0.05rem); + width: 0.05rem; + top: calc(-0.5 * 0.6rem + 0.05rem); + left: 0; + background-color: #ccc; + background-color: var(--clr-table-border-color, #ccc); } .datagrid-table - .datagrid-column - .datagrid-column-separator - .datagrid-column-handle { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - position: absolute; - width: 0.65rem; - right: -0.3rem; - top: -0.3rem; - cursor: col-resize; - height: calc(100% + 0.6rem - 0.05rem); - z-index: 501; + .datagrid-column + .datagrid-column-separator + .datagrid-column-handle { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + position: absolute; + width: 0.65rem; + right: -0.3rem; + top: -0.3rem; + cursor: col-resize; + height: calc(100% + 0.6rem - 0.05rem); + z-index: 501; } button.datagrid-table - .datagrid-column - .datagrid-column-separator - .datagrid-column-handle { - cursor: pointer; + .datagrid-column + .datagrid-column-separator + .datagrid-column-handle { + cursor: pointer; } .datagrid-table - .datagrid-column - .datagrid-column-separator - .datagrid-column-resize-tracker { - position: absolute; - top: -0.6rem; - display: none; - width: 0.05rem; - height: 0; - border-right-style: dotted; - border-right-color: #79c6e6; - border-right-color: var(--clr-color-action-300, #79c6e6); - border-right-width: 0.05rem; - border-right-width: var(--clr-global-borderwidth, 0.05rem); - -webkit-transform: translateX(0); - transform: translateX(0); - cursor: col-resize; + .datagrid-column + .datagrid-column-separator + .datagrid-column-resize-tracker { + position: absolute; + top: -0.6rem; + display: none; + width: 0.05rem; + height: 0; + border-right-style: dotted; + border-right-color: #79c6e6; + border-right-color: var(--clr-color-action-300, #79c6e6); + border-right-width: 0.05rem; + border-right-width: var(--clr-global-borderwidth, 0.05rem); + -webkit-transform: translateX(0); + transform: translateX(0); + cursor: col-resize; } .datagrid-table - .datagrid-column - .datagrid-column-separator - .datagrid-column-resize-tracker.on-arrow-key-resize { - -webkit-transition: -webkit-transform 0.2s ease-out; - transition: -webkit-transform 0.2s ease-out; - transition: transform 0.2s ease-out; - transition: - transform 0.2s ease-out, - -webkit-transform 0.2s ease-out; + .datagrid-column + .datagrid-column-separator + .datagrid-column-resize-tracker.on-arrow-key-resize { + -webkit-transition: -webkit-transform 0.2s ease-out; + transition: -webkit-transform 0.2s ease-out; + transition: transform 0.2s ease-out; + transition: + transform 0.2s ease-out, + -webkit-transform 0.2s ease-out; } .datagrid-table .datagrid-column .datagrid-column-separator .exceeded-max { - border-right: 0.05rem dotted rgba(219, 33, 0, 0.3); + border-right: 0.05rem dotted rgba(219, 33, 0, 0.3); } .datagrid-table .datagrid-column .datagrid-signpost-trigger .signpost { - margin: -0.36rem 0; - height: 1.236rem; + margin: -0.36rem 0; + height: 1.236rem; } .datagrid-table - .datagrid-column - .datagrid-signpost-trigger - .signpost - .signpost-trigger { - height: inherit; - line-height: 1.2rem; + .datagrid-column + .datagrid-signpost-trigger + .signpost + .signpost-trigger { + height: inherit; + line-height: 1.2rem; } .datagrid-table .datagrid-column.datagrid-expandable-caret, .datagrid-table .datagrid-column.datagrid-row-actions, .datagrid-table .datagrid-column.datagrid-select { - max-width: 1.9rem; - min-width: 1.9rem; + max-width: 1.9rem; + min-width: 1.9rem; } .datagrid-table .datagrid-cell { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - text-align: left; - min-width: 4.8rem; - border: none; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + text-align: left; + min-width: 4.8rem; + border: none; } .datagrid-table .datagrid-cell.datagrid-fixed-width { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; } .datagrid-table .datagrid-cell.datagrid-fixed-column { - -webkit-box-flex: 0; - -ms-flex: 0 0 1.9rem; - flex: 0 0 1.9rem; - max-width: 1.9rem; - min-width: 1.9rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 1.9rem; + flex: 0 0 1.9rem; + max-width: 1.9rem; + min-width: 1.9rem; } .datagrid-table .datagrid-cell.datagrid-row-actions { - background: 0 0; + background: 0 0; } .datagrid-table .datagrid-cell.datagrid-expandable-caret { - padding: 0.1rem 0.2rem 0.15rem; - text-align: center; + padding: 0.1rem 0.2rem 0.15rem; + text-align: center; } .datagrid-table .datagrid-cell .datagrid-action-toggle { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; } button.datagrid-table .datagrid-cell .datagrid-action-toggle { - cursor: pointer; + cursor: pointer; } .datagrid-table .datagrid-cell .datagrid-action-toggle cds-icon { - color: #8c8c8c; - color: var(--clr-datagrid-icon-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-datagrid-icon-color, #8c8c8c); } .datagrid-table .datagrid-cell .datagrid-action-toggle:active cds-icon { - color: #666; - color: var(--clr-datagrid-action-toggle, #666); + color: #666; + color: var(--clr-datagrid-action-toggle, #666); } .datagrid-table .datagrid-cell .clr-toggle-wrapper { - margin-top: -0.66rem; - padding-top: 0.42rem; + margin-top: -0.66rem; + padding-top: 0.42rem; } .datagrid-table .datagrid-placeholder-container { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - border-top-style: solid; - border-top-color: #ccc; - border-top-color: var(--clr-table-border-color, #ccc); - border-top-width: 0.05rem; - border-top-width: var(--clr-global-borderwidth, 0.05rem); + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + border-top-style: solid; + border-top-color: #ccc; + border-top-color: var(--clr-table-border-color, #ccc); + border-top-width: 0.05rem; + border-top-width: var(--clr-global-borderwidth, 0.05rem); } .datagrid-table .datagrid-placeholder { - background: #fff; - background: var(--clr-table-bgcolor, #fff); - width: 100%; + background: #fff; + background: var(--clr-table-bgcolor, #fff); + width: 100%; } .datagrid-table .datagrid-placeholder.datagrid-empty { - border-top: 0; - padding: 0.6rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-flow: column nowrap; - flex-flow: column nowrap; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: start; - -ms-flex-pack: start; - justify-content: flex-start; - font-size: 0.8rem; - font-size: var(--clr-datagrid-placeholder-font-size, 0.8rem); - color: #b3b3b3; - color: var(--clr-datagrid-placeholder-color, #b3b3b3); + border-top: 0; + padding: 0.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -ms-flex-pack: start; + justify-content: flex-start; + font-size: 0.8rem; + font-size: var(--clr-datagrid-placeholder-font-size, 0.8rem); + color: #b3b3b3; + color: var(--clr-datagrid-placeholder-color, #b3b3b3); } .datagrid-table .datagrid-placeholder .datagrid-placeholder-image { - height: 3rem; - width: 3rem; - margin-bottom: 0.6rem; - background-repeat: no-repeat; - background-size: contain; - background-position: center; - background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2060%2072%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cellipse%20id%3D%22path-1%22%20cx%3D%2230%22%20cy%3D%2261.7666667%22%20rx%3D%2215.4512904%22%20ry%3D%224.73333333%22%3E%3C%2Fellipse%3E%0A%20%20%20%20%20%20%20%20%3Cmask%20id%3D%22mask-2%22%20maskContentUnits%3D%22userSpaceOnUse%22%20maskUnits%3D%22objectBoundingBox%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2230.9025808%22%20height%3D%229.46666667%22%20fill%3D%22white%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cuse%20xlink%3Ahref%3D%22%23path-1%22%3E%3C%2Fuse%3E%0A%20%20%20%20%20%20%20%20%3C%2Fmask%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Artboard%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cuse%20id%3D%22Oval-10%22%20stroke%3D%22%23C1DFEF%22%20mask%3D%22url(%23mask-2)%22%20stroke-width%3D%222.8%22%20stroke-linecap%3D%22square%22%20stroke-dasharray%3D%223%2C6%2C3%2C5%22%20xlink%3Ahref%3D%22%23path-1%22%3E%3C%2Fuse%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M38.4613647%2C18.1642456%20L30.9890137%2C34.9141846%20L31%2C47%20L32.5977783%2C46.5167236%20L32.5977783%2C34.9141846%20L51.0673218%2C15.7560425%20C51.0673218%2C15.7560425%2048.6295166%2C16.6542969%2044.9628906%2C17.3392334%20C41.2962646%2C18.0241699%2038.4613647%2C18.1642456%2038.4613647%2C18.1642456%20Z%22%20id%3D%22Path-195%22%20fill%3D%22%23C1DFEF%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M4.74639226%2C12.5661855%20L4.62065726%2C12.1605348%20L5.3515414%2C11.1625044%20L5.77622385%2C11.159939%20L6.20936309%2C12.5573481%20L4.74639226%2C12.5661855%20Z%20M6.20936309%2C12.5573481%20L6.32542632%2C12.9317954%20L28.4963855%2C34.8796718%20L28.4963855%2C47.8096691%20L32.6%2C46.4836513%20L32.6%2C34.8992365%20L53.973494%2C12.7035813%20L53.973494%2C12.2688201%20L6.20936309%2C12.5573481%20Z%20M55.373494%2C10.8603376%20L55.373494%2C13.2680664%20L34%2C35.4637216%20L34%2C47.5025401%20L27.0963855%2C49.7333333%20L27.0963855%2C35.4637219%20L5.09179688%2C13.680542%20L4.31325301%2C11.1687764%20L55.373494%2C10.8603376%20Z%22%20id%3D%22Path-149%22%20fill%3D%22%237FBDDD%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cellipse%20id%3D%22Oval-9%22%20fill%3D%22%23FFFFFF%22%20cx%3D%2230%22%20cy%3D%2211.785654%22%20rx%3D%2226%22%20ry%3D%226.78565401%22%3E%3C%2Fellipse%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M30%2C17.171308%20C36.8772177%2C17.171308%2043.3112282%2C16.4610701%2048.0312371%2C15.2292106%20C50.2777611%2C14.6428977%2052.0507619%2C13.9579677%2053.2216231%2C13.2354973%20C54.1938565%2C12.6355886%2054.6%2C12.1175891%2054.6%2C11.785654%20C54.6%2C11.4537189%2054.1938565%2C10.9357194%2053.2216231%2C10.3358107%20C52.0507619%2C9.61334032%2050.2777611%2C8.92841034%2048.0312371%2C8.34209746%20C43.3112282%2C7.11023795%2036.8772177%2C6.4%2030%2C6.4%20C23.1227823%2C6.4%2016.6887718%2C7.11023795%2011.9687629%2C8.34209746%20C9.72223886%2C8.92841034%207.94923814%2C9.61334032%206.77837689%2C10.3358107%20C5.8061435%2C10.9357194%205.4%2C11.4537189%205.4%2C11.785654%20C5.4%2C12.1175891%205.8061435%2C12.6355886%206.77837689%2C13.2354973%20C7.94923814%2C13.9579677%209.72223886%2C14.6428977%2011.9687629%2C15.2292106%20C16.6887718%2C16.4610701%2023.1227823%2C17.171308%2030%2C17.171308%20Z%20M30%2C18.571308%20C15.6405965%2C18.571308%204%2C15.5332672%204%2C11.785654%20C4%2C8.03804078%2015.6405965%2C5%2030%2C5%20C44.3594035%2C5%2056%2C8.03804078%2056%2C11.785654%20C56%2C15.5332672%2044.3594035%2C18.571308%2030%2C18.571308%20Z%22%20id%3D%22Oval-9-Copy%22%20fill%3D%22%237FBDDD%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M18.2608643%2C7.14562988%20L22.727356%2C16.9047241%20C22.727356%2C16.9047241%2015.3006592%2C16.3911743%2010.276001%2C14.7511597%20C5.25134277%2C13.111145%205.38031006%2C11.8284302%205.38031006%2C11.6882935%20C5.38031006%2C10.4832831%208.16633152%2C9.41877716%2011.114563%2C8.57324219%20C14.549319%2C7.58817492%2018.2608643%2C7.14562988%2018.2608643%2C7.14562988%20Z%22%20id%3D%22Path-196%22%20fill%3D%22%23C1DFEF%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E'); + height: 3rem; + width: 3rem; + margin-bottom: 0.6rem; + background-repeat: no-repeat; + background-size: contain; + background-position: center; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%220%200%2060%2072%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%3E%0A%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3Cellipse%20id%3D%22path-1%22%20cx%3D%2230%22%20cy%3D%2261.7666667%22%20rx%3D%2215.4512904%22%20ry%3D%224.73333333%22%3E%3C%2Fellipse%3E%0A%20%20%20%20%20%20%20%20%3Cmask%20id%3D%22mask-2%22%20maskContentUnits%3D%22userSpaceOnUse%22%20maskUnits%3D%22objectBoundingBox%22%20x%3D%220%22%20y%3D%220%22%20width%3D%2230.9025808%22%20height%3D%229.46666667%22%20fill%3D%22white%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cuse%20xlink%3Ahref%3D%22%23path-1%22%3E%3C%2Fuse%3E%0A%20%20%20%20%20%20%20%20%3C%2Fmask%3E%0A%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%3Cg%20id%3D%22Page-1%22%20stroke%3D%22none%22%20stroke-width%3D%221%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%0A%20%20%20%20%20%20%20%20%3Cg%20id%3D%22Artboard%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cuse%20id%3D%22Oval-10%22%20stroke%3D%22%23C1DFEF%22%20mask%3D%22url(%23mask-2)%22%20stroke-width%3D%222.8%22%20stroke-linecap%3D%22square%22%20stroke-dasharray%3D%223%2C6%2C3%2C5%22%20xlink%3Ahref%3D%22%23path-1%22%3E%3C%2Fuse%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M38.4613647%2C18.1642456%20L30.9890137%2C34.9141846%20L31%2C47%20L32.5977783%2C46.5167236%20L32.5977783%2C34.9141846%20L51.0673218%2C15.7560425%20C51.0673218%2C15.7560425%2048.6295166%2C16.6542969%2044.9628906%2C17.3392334%20C41.2962646%2C18.0241699%2038.4613647%2C18.1642456%2038.4613647%2C18.1642456%20Z%22%20id%3D%22Path-195%22%20fill%3D%22%23C1DFEF%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M4.74639226%2C12.5661855%20L4.62065726%2C12.1605348%20L5.3515414%2C11.1625044%20L5.77622385%2C11.159939%20L6.20936309%2C12.5573481%20L4.74639226%2C12.5661855%20Z%20M6.20936309%2C12.5573481%20L6.32542632%2C12.9317954%20L28.4963855%2C34.8796718%20L28.4963855%2C47.8096691%20L32.6%2C46.4836513%20L32.6%2C34.8992365%20L53.973494%2C12.7035813%20L53.973494%2C12.2688201%20L6.20936309%2C12.5573481%20Z%20M55.373494%2C10.8603376%20L55.373494%2C13.2680664%20L34%2C35.4637216%20L34%2C47.5025401%20L27.0963855%2C49.7333333%20L27.0963855%2C35.4637219%20L5.09179688%2C13.680542%20L4.31325301%2C11.1687764%20L55.373494%2C10.8603376%20Z%22%20id%3D%22Path-149%22%20fill%3D%22%237FBDDD%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cellipse%20id%3D%22Oval-9%22%20fill%3D%22%23FFFFFF%22%20cx%3D%2230%22%20cy%3D%2211.785654%22%20rx%3D%2226%22%20ry%3D%226.78565401%22%3E%3C%2Fellipse%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M30%2C17.171308%20C36.8772177%2C17.171308%2043.3112282%2C16.4610701%2048.0312371%2C15.2292106%20C50.2777611%2C14.6428977%2052.0507619%2C13.9579677%2053.2216231%2C13.2354973%20C54.1938565%2C12.6355886%2054.6%2C12.1175891%2054.6%2C11.785654%20C54.6%2C11.4537189%2054.1938565%2C10.9357194%2053.2216231%2C10.3358107%20C52.0507619%2C9.61334032%2050.2777611%2C8.92841034%2048.0312371%2C8.34209746%20C43.3112282%2C7.11023795%2036.8772177%2C6.4%2030%2C6.4%20C23.1227823%2C6.4%2016.6887718%2C7.11023795%2011.9687629%2C8.34209746%20C9.72223886%2C8.92841034%207.94923814%2C9.61334032%206.77837689%2C10.3358107%20C5.8061435%2C10.9357194%205.4%2C11.4537189%205.4%2C11.785654%20C5.4%2C12.1175891%205.8061435%2C12.6355886%206.77837689%2C13.2354973%20C7.94923814%2C13.9579677%209.72223886%2C14.6428977%2011.9687629%2C15.2292106%20C16.6887718%2C16.4610701%2023.1227823%2C17.171308%2030%2C17.171308%20Z%20M30%2C18.571308%20C15.6405965%2C18.571308%204%2C15.5332672%204%2C11.785654%20C4%2C8.03804078%2015.6405965%2C5%2030%2C5%20C44.3594035%2C5%2056%2C8.03804078%2056%2C11.785654%20C56%2C15.5332672%2044.3594035%2C18.571308%2030%2C18.571308%20Z%22%20id%3D%22Oval-9-Copy%22%20fill%3D%22%237FBDDD%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpath%20d%3D%22M18.2608643%2C7.14562988%20L22.727356%2C16.9047241%20C22.727356%2C16.9047241%2015.3006592%2C16.3911743%2010.276001%2C14.7511597%20C5.25134277%2C13.111145%205.38031006%2C11.8284302%205.38031006%2C11.6882935%20C5.38031006%2C10.4832831%208.16633152%2C9.41877716%2011.114563%2C8.57324219%20C14.549319%2C7.58817492%2018.2608643%2C7.14562988%2018.2608643%2C7.14562988%20Z%22%20id%3D%22Path-196%22%20fill%3D%22%23C1DFEF%22%3E%3C%2Fpath%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fg%3E%0A%3C%2Fsvg%3E"); } .datagrid-table .datagrid-hidden-column.datagrid-cell, .datagrid-table .datagrid-hidden-column.datagrid-column { - display: none; + display: none; } .datagrid-row-replaced - .datagrid-scrolling-cells - .datagrid-cell:not(.datagrid-expandable-caret):not( - .datagrid-row-actions - ):not(.datagrid-select) { - display: none; + .datagrid-scrolling-cells + .datagrid-cell:not(.datagrid-expandable-caret):not(.datagrid-row-actions):not( + .datagrid-select + ) { + display: none; } .datagrid-row-replaced .datagrid-row-detail .datagrid-cell { - display: block; - padding-top: 0.55rem; + display: block; + padding-top: 0.55rem; } .datagrid-row-replaced - .datagrid-row-detail - .datagrid-cell.datagrid-hidden-column { - display: none; + .datagrid-row-detail + .datagrid-cell.datagrid-hidden-column { + display: none; } .datagrid-row-replaced .datagrid-row-detail .datagrid-expandable-caret { - padding-top: 0.1rem; + padding-top: 0.1rem; } .datagrid-row-replaced .datagrid-row-detail.datagrid-container { - border-top: 0.05rem solid #ccc; + border-top: 0.05rem solid #ccc; } .datagrid-row-replaced .datagrid-row-detail.datagrid-container .datagrid-cell { - border-top: none; + border-top: none; } .datagrid-footer { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row nowrap; - flex-flow: row nowrap; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - padding: 0 0.6rem; - line-height: 1.65rem; - font-size: 0.55rem; - background-color: #fafafa; - background-color: var(--clr-thead-bgcolor, #fafafa); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-table-footer-border-top-color, #ccc); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-top: none; - border-radius: 0; - border-bottom-right-radius: 0.15rem; - border-bottom-right-radius: var(--clr-global-borderradius, 0.15rem); - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-global-borderradius, 0.15rem); + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + padding: 0 0.6rem; + line-height: 1.65rem; + font-size: 0.55rem; + background-color: #fafafa; + background-color: var(--clr-thead-bgcolor, #fafafa); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-table-footer-border-top-color, #ccc); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-top: none; + border-radius: 0; + border-bottom-right-radius: 0.15rem; + border-bottom-right-radius: var(--clr-global-borderradius, 0.15rem); + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-global-borderradius, 0.15rem); } .datagrid-footer .pagination { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - line-height: 1.8rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + line-height: 1.8rem; } .datagrid-footer .pagination-size { - display: block; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - white-space: nowrap; - text-align: right; + display: block; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + white-space: nowrap; + text-align: right; } .datagrid-footer .pagination-size .clr-select-wrapper::after { - top: 0.6rem; + top: 0.6rem; } .datagrid-footer .pagination-size .clr-page-size-select { - font-size: 100%; - margin-left: 0.6rem; - height: 1.2rem; - line-height: 1.2rem; - vertical-align: middle; + font-size: 100%; + margin-left: 0.6rem; + height: 1.2rem; + line-height: 1.2rem; + vertical-align: middle; } .datagrid-footer .pagination-size + .pagination-description { - margin-left: 1.2rem; + margin-left: 1.2rem; } .datagrid-footer .pagination-description { - white-space: nowrap; + white-space: nowrap; } .datagrid-footer .pagination-list { - margin-left: 1.2rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + margin-left: 1.2rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; } .datagrid-footer .column-switch-wrapper { - position: relative; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; + position: relative; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; } .datagrid-footer .column-switch-wrapper.active .column-toggle--action cds-icon { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); } .datagrid-footer .column-switch-wrapper .column-toggle--action { - min-width: 0.9rem; - padding-left: 0; - padding-right: 0; + min-width: 0.9rem; + padding-left: 0; + padding-right: 0; } .datagrid-footer .column-switch-wrapper .column-toggle--action cds-icon { - color: #b3b3b3; - color: var(--clr-color-neutral-500, #b3b3b3); + color: #b3b3b3; + color: var(--clr-color-neutral-500, #b3b3b3); } .datagrid-footer .column-switch-wrapper .column-toggle--action:hover cds-icon { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); } .datagrid-footer .column-switch-wrapper .column-switch { - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - background-color: #fff; - background-color: var(--clr-datagrid-popover-bg-color, #fff); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - padding: 0.9rem; - border-style: solid; - border-top: none; - width: 12.5rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - z-index: 1060; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + background-color: #fff; + background-color: var(--clr-datagrid-popover-bg-color, #fff); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + padding: 0.9rem; + border-style: solid; + border-top: none; + width: 12.5rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + z-index: 1060; } .datagrid-footer .column-switch-wrapper .column-switch .switch-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - font-weight: 400; - font-size: 0.8rem; - padding-bottom: 0.6rem; - line-height: 1.2rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + font-weight: 400; + font-size: 0.8rem; + padding-bottom: 0.6rem; + line-height: 1.2rem; } .datagrid-footer .column-switch-wrapper .column-switch .switch-header button { - min-width: 0.9rem; - margin: 0; - padding: 0; - color: #b3b3b3; - color: var(--clr-color-neutral-500, #b3b3b3); + min-width: 0.9rem; + margin: 0; + padding: 0; + color: #b3b3b3; + color: var(--clr-color-neutral-500, #b3b3b3); } .datagrid-footer - .column-switch-wrapper - .column-switch - .switch-header - button:hover { - color: #0072a3; - color: var(--clr-color-action-600, #0072a3); + .column-switch-wrapper + .column-switch + .switch-header + button:hover { + color: #0072a3; + color: var(--clr-color-action-600, #0072a3); } .datagrid-footer .column-switch-wrapper .column-switch .switch-content { - max-height: 15rem; - overflow-y: auto; - min-height: 2.4rem; + max-height: 15rem; + overflow-y: auto; + min-height: 2.4rem; } .datagrid-footer .column-switch-wrapper .column-switch .switch-content li { - line-height: 1.2rem; - padding-left: 0.1rem; + line-height: 1.2rem; + padding-left: 0.1rem; } .datagrid-footer .column-switch-wrapper .column-switch .switch-footer .btn { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .datagrid-footer - .column-switch-wrapper - .column-switch - .switch-footer - .action-right { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + .column-switch-wrapper + .column-switch + .switch-footer + .action-right { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .datagrid-footer .column-switch-wrapper .column-switch { - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - background-color: #fff; - background-color: var(--clr-datagrid-popover-bg-color, #fff); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - border-style: solid; - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - padding: 0.9rem; - width: 12.5rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - z-index: 1060; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + background-color: #fff; + background-color: var(--clr-datagrid-popover-bg-color, #fff); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + border-style: solid; + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + padding: 0.9rem; + width: 12.5rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + z-index: 1060; } .datagrid-footer .column-switch-wrapper .column-switch .switch-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - font-weight: 400; - font-size: 0.8rem; - padding-bottom: 0.6rem; - line-height: 1.2rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + font-weight: 400; + font-size: 0.8rem; + padding-bottom: 0.6rem; + line-height: 1.2rem; } .datagrid-footer .column-switch-wrapper .column-switch .switch-header button { - min-width: 0.9rem; - margin: 0; - padding: 0; - color: #b3b3b3; - color: var(--clr-datagrid-column-switch-header-font-color, #b3b3b3); + min-width: 0.9rem; + margin: 0; + padding: 0; + color: #b3b3b3; + color: var(--clr-datagrid-column-switch-header-font-color, #b3b3b3); } .datagrid-footer - .column-switch-wrapper - .column-switch - .switch-header - button:hover { - color: #0072a3; - color: var(--clr-datagrid-column-switch-header-font-hover-color, #0072a3); + .column-switch-wrapper + .column-switch + .switch-header + button:hover { + color: #0072a3; + color: var(--clr-datagrid-column-switch-header-font-hover-color, #0072a3); } .datagrid-footer .column-switch-wrapper .column-switch .switch-content { - max-height: 15rem; - overflow-y: auto; - min-height: 1.25rem; + max-height: 15rem; + overflow-y: auto; + min-height: 1.25rem; } .datagrid-footer .column-switch-wrapper .column-switch .switch-content li { - line-height: 1.2rem; - padding-left: 0.1rem; + line-height: 1.2rem; + padding-left: 0.1rem; } .datagrid-footer .column-switch-wrapper .column-switch .switch-footer .btn { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .datagrid-footer - .column-switch-wrapper - .column-switch - .switch-footer - .action-right { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + .column-switch-wrapper + .column-switch + .switch-footer + .action-right { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .clr-form-control-disabled - .datagrid-footer-select.clr-checkbox-wrapper - input[type='checkbox']:checked - + label { - top: 0.2rem; - cursor: default; - margin-right: 0.45rem; + .datagrid-footer-select.clr-checkbox-wrapper + input[type="checkbox"]:checked + + label { + top: 0.2rem; + cursor: default; + margin-right: 0.45rem; } .clr-form-control-disabled - .datagrid-footer-select.clr-checkbox-wrapper - input[type='checkbox']:checked - + label::before { - background-color: #8c8c8c; - background-color: var(--clr-color-neutral-600, #8c8c8c); + .datagrid-footer-select.clr-checkbox-wrapper + input[type="checkbox"]:checked + + label::before { + background-color: #8c8c8c; + background-color: var(--clr-color-neutral-600, #8c8c8c); } .clr-form-control-disabled - .datagrid-footer-select.clr-checkbox-wrapper - input[type='checkbox']:checked - + label::after { - border-left-color: #fff; - border-left-color: var(--clr-color-neutral-0, #fff); - border-bottom-color: #fff; - border-bottom-color: var(--clr-color-neutral-0, #fff); + .datagrid-footer-select.clr-checkbox-wrapper + input[type="checkbox"]:checked + + label::after { + border-left-color: #fff; + border-left-color: var(--clr-color-neutral-0, #fff); + border-bottom-color: #fff; + border-bottom-color: var(--clr-color-neutral-0, #fff); } .datagrid-spinner { - position: absolute; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - width: 100%; - top: 0.6rem; - height: calc(100% - 0.6rem); - background-color: rgba(255, 255, 255, 0.6); - background-color: var( - --clr-datagrid-loading-background, - rgba(255, 255, 255, 0.6) - ); - z-index: 590; + position: absolute; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; + top: 0.6rem; + height: calc(100% - 0.6rem); + background-color: rgba(255, 255, 255, 0.6); + background-color: var( + --clr-datagrid-loading-background, + rgba(255, 255, 255, 0.6) + ); + z-index: 590; } .datagrid-select .clr-control-label { - min-height: 0.6rem; - margin-top: -0.24rem; - padding-left: 0.7rem; + min-height: 0.6rem; + margin-top: -0.24rem; + padding-left: 0.7rem; } .datagrid-compact .datagrid-header { - min-height: 1.2rem; + min-height: 1.2rem; } .datagrid-compact .datagrid-column .datagrid-column-separator::after { - height: calc(100% + 0.5 * 0.6rem - 0.05rem); - top: calc(-0.25 * 0.6rem + 0.05rem); + height: calc(100% + 0.5 * 0.6rem - 0.05rem); + top: calc(-0.25 * 0.6rem + 0.05rem); } .datagrid-compact .datagrid-cell cds-icon { - margin-top: -0.2rem; - margin-bottom: -0.15rem; - -webkit-transform: translateY(-0.05rem); - transform: translateY(-0.05rem); -} -.datagrid-compact .datagrid-cell .badge { - margin-top: -0.15rem; - margin-bottom: -0.05rem; -} -.datagrid-compact .datagrid-expandable-caret { - text-align: center; -} -.datagrid-compact .datagrid-expandable-caret .spinner { - margin-top: 0.15rem; -} -.datagrid-compact .datagrid-expandable-caret .datagrid-expandable-caret-button { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - height: 1.2rem; - width: 1.2rem; - outline-offset: -0.2rem; + margin-top: -0.2rem; + margin-bottom: -0.15rem; + -webkit-transform: translateY(-0.05rem); + transform: translateY(-0.05rem); +} +.datagrid-compact .datagrid-cell .badge { + margin-top: -0.15rem; + margin-bottom: -0.05rem; +} +.datagrid-compact .datagrid-expandable-caret { + text-align: center; +} +.datagrid-compact .datagrid-expandable-caret .spinner { + margin-top: 0.15rem; +} +.datagrid-compact .datagrid-expandable-caret .datagrid-expandable-caret-button { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + height: 1.2rem; + width: 1.2rem; + outline-offset: -0.2rem; } button.datagrid-compact - .datagrid-expandable-caret - .datagrid-expandable-caret-button { - cursor: pointer; + .datagrid-expandable-caret + .datagrid-expandable-caret-button { + cursor: pointer; } .datagrid-compact .datagrid-expandable-caret .datagrid-expandable-caret-icon { - margin: 0; + margin: 0; } .datagrid-compact .datagrid-expandable-caret.datagrid-cell { - padding: 0; + padding: 0; } .datagrid-compact .datagrid-expandable-caret.datagrid-column { - padding-top: 0.3rem; - padding-bottom: 0.25rem; + padding-top: 0.3rem; + padding-bottom: 0.25rem; } .datagrid-compact - .datagrid-signpost-trigger - .signpost - .signpost-trigger - cds-icon:not( - [shape='info-circle'], - [shape='exclamation-triangle'], - [shape='exclamation-circle'], - [shape='check-circle'], - [shape='info'], - [shape='error'] - ) { - height: 1.05rem; - width: 1.05rem; + .datagrid-signpost-trigger + .signpost + .signpost-trigger + cds-icon:not( + [shape="info-circle"], + [shape="exclamation-triangle"], + [shape="exclamation-circle"], + [shape="check-circle"], + [shape="info"], + [shape="error"] + ) { + height: 1.05rem; + width: 1.05rem; } .datagrid-compact .datagrid-footer { - padding: 0 0.6rem; - line-height: 1.15rem; + padding: 0 0.6rem; + line-height: 1.15rem; } .datagrid-compact .datagrid-footer .pagination { - line-height: 1.2rem; + line-height: 1.2rem; } .datagrid-compact - .datagrid-footer - .column-switch-wrapper - .column-toggle--action { - margin: 0; - outline-offset: -0.2rem; + .datagrid-footer + .column-switch-wrapper + .column-toggle--action { + margin: 0; + outline-offset: -0.2rem; } .datagrid-compact - .datagrid-footer - .clr-form-control-disabled - .datagrid-footer-select.clr-checkbox-wrapper - input[type='checkbox']:checked - + label { - top: 0; + .datagrid-footer + .clr-form-control-disabled + .datagrid-footer-select.clr-checkbox-wrapper + input[type="checkbox"]:checked + + label { + top: 0; } .datagrid-footer-description { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - -ms-flex-wrap: nowrap; - flex-wrap: nowrap; - white-space: nowrap; - display: block; - text-align: right; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + white-space: nowrap; + display: block; + text-align: right; } .pagination-list { - list-style: none; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row nowrap; - flex-flow: row nowrap; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; + list-style: none; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; } .pagination-list .pagination-current { - background: 0 0; - background-color: #fff; - background-color: var(--clr-forms-textarea-background-color, #fff); - border-color: #ccc; - border-color: var(--clr-datagrid-pagination-input-border-color, #ccc); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - line-height: 1.2rem; - font-size: 0.55rem; - min-width: 1.2rem; - text-align: center; - -webkit-transition: none !important; - transition: none !important; + background: 0 0; + background-color: #fff; + background-color: var(--clr-forms-textarea-background-color, #fff); + border-color: #ccc; + border-color: var(--clr-datagrid-pagination-input-border-color, #ccc); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + line-height: 1.2rem; + font-size: 0.55rem; + min-width: 1.2rem; + text-align: center; + -webkit-transition: none !important; + transition: none !important; } .pagination-list .pagination-current.clr-focus, .pagination-list .pagination-current:focus { - background: 0 0; - border: 0.05rem solid; - border-color: #49aeda; - border-color: var( - --clr-datagrid-pagination-input-border-focus-color, - #49aeda - ); - -webkit-box-shadow: 0 0 0.05rem #49aeda; - box-shadow: 0 0 0.05rem #49aeda; - -webkit-box-shadow: 0 0 var(--clr-global-borderwidth, 0.05rem) - var(--clr-datagrid-pagination-input-border-focus-color, #49aeda); - box-shadow: 0 0 var(--clr-global-borderwidth, 0.05rem) - var(--clr-datagrid-pagination-input-border-focus-color, #49aeda); + background: 0 0; + border: 0.05rem solid; + border-color: #49aeda; + border-color: var( + --clr-datagrid-pagination-input-border-focus-color, + #49aeda + ); + -webkit-box-shadow: 0 0 0.05rem #49aeda; + box-shadow: 0 0 0.05rem #49aeda; + -webkit-box-shadow: 0 0 var(--clr-global-borderwidth, 0.05rem) + var(--clr-datagrid-pagination-input-border-focus-color, #49aeda); + box-shadow: 0 0 var(--clr-global-borderwidth, 0.05rem) + var(--clr-datagrid-pagination-input-border-focus-color, #49aeda); } .pagination-list > * { - padding: 0 0.12rem; + padding: 0 0.12rem; } .pagination-list .pagination-first, .pagination-list .pagination-last, .pagination-list .pagination-next, .pagination-list .pagination-previous { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - height: 0.7rem; - width: 0.7rem; - background-repeat: no-repeat; - background-size: contain; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 0.7rem; + width: 0.7rem; + background-repeat: no-repeat; + background-size: contain; } .pagination-list .pagination-first:disabled, .pagination-list .pagination-last:disabled, .pagination-list .pagination-next:disabled, .pagination-list .pagination-previous:disabled { - color: #8c8c8c; - color: var(--clr-datagrid-pagination-btn-disabled-color, #8c8c8c); - opacity: 0.56; - opacity: var(--clr-datagrid-pagination-btn-disabled-opacity, 0.56); + color: #8c8c8c; + color: var(--clr-datagrid-pagination-btn-disabled-color, #8c8c8c); + opacity: 0.56; + opacity: var(--clr-datagrid-pagination-btn-disabled-opacity, 0.56); } .pagination-list .pagination-first, .pagination-list .pagination-previous { - margin-right: 0.6rem; + margin-right: 0.6rem; } .pagination-list .pagination-last, .pagination-list .pagination-next { - margin-left: 0.6rem; + margin-left: 0.6rem; } .pagination-list button { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - color: #666; - color: var(--clr-datagrid-pagination-btn-color, #666); - cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + color: #666; + color: var(--clr-datagrid-pagination-btn-color, #666); + cursor: pointer; } button.pagination-list button { - cursor: pointer; + cursor: pointer; } .datagrid-cell-width-zero { - border: 0 !important; - padding: 0 !important; - width: 0; - -webkit-box-flex: 0 !important; - -ms-flex: 0 0 auto !important; - flex: 0 0 auto !important; - min-width: 0 !important; - display: block !important; + border: 0 !important; + padding: 0 !important; + width: 0; + -webkit-box-flex: 0 !important; + -ms-flex: 0 0 auto !important; + flex: 0 0 auto !important; + min-width: 0 !important; + display: block !important; } .datagrid-outer-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - overflow: auto; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + overflow: auto; } .datagrid-inner-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-flex: 1; - -ms-flex-positive: 1; - flex-grow: 1; - overflow: auto; - min-width: 12rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + overflow: auto; + min-width: 12rem; } .datagrid-detail-open .datagrid { - border-top-right-radius: 0; - border-right: none; + border-top-right-radius: 0; + border-right: none; } .datagrid-detail-open .datagrid-inner-wrapper { - width: 34%; + width: 34%; } .datagrid-detail-open .datagrid-placeholder-container, .datagrid-detail-open .datagrid-row { - border-right: 0.05rem solid #ccc; + border-right: 0.05rem solid #ccc; } .datagrid-detail-open .datagrid-footer { - border-bottom-right-radius: 0; + border-bottom-right-radius: 0; } .datagrid-detail-open .pagination { - width: 100%; + width: 100%; } .datagrid-detail-open .pagination-description-compact { - text-align: left; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; + text-align: left; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; } .datagrid-detail-open .datagrid-footer .pagination-list { - margin-right: 0; + margin-right: 0; } .datagrid-row-detail-open { - position: relative; + position: relative; } .datagrid-row-detail-open:before { - content: ''; - display: inline-block; - position: absolute; - border: 0.5rem solid transparent; - border-color: transparent; - border-right-color: #ccc; - border-right-color: var(--clr-table-border-color, #ccc); - top: 0.5rem; - right: 0; + content: ""; + display: inline-block; + position: absolute; + border: 0.5rem solid transparent; + border-color: transparent; + border-right-color: #ccc; + border-right-color: var(--clr-table-border-color, #ccc); + top: 0.5rem; + right: 0; } .datagrid-row-detail-open:after { - content: ''; - display: inline-block; - position: absolute; - border: 0.45rem solid transparent; - border-right-color: #fff; - border-right-color: var(--clr-datagrid-popover-bg-color, #fff); - top: 0.55rem; - right: -0.05rem; + content: ""; + display: inline-block; + position: absolute; + border: 0.45rem solid transparent; + border-right-color: #fff; + border-right-color: var(--clr-datagrid-popover-bg-color, #fff); + top: 0.55rem; + right: -0.05rem; } .datagrid-detail-pane { - margin-top: 0.6rem; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-datagrid-default-border-color, #ccc); - border-left: none; - border-top-right-radius: 0.15rem; - border-top-right-radius: var(--clr-global-borderradius, 0.15rem); - border-bottom-right-radius: 0.15rem; - border-bottom-right-radius: var(--clr-global-borderradius, 0.15rem); - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - overflow: hidden; - display: block; - -webkit-box-flex: 2; - -ms-flex-positive: 2; - flex-grow: 2; - width: 66%; + margin-top: 0.6rem; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-datagrid-default-border-color, #ccc); + border-left: none; + border-top-right-radius: 0.15rem; + border-top-right-radius: var(--clr-global-borderradius, 0.15rem); + border-bottom-right-radius: 0.15rem; + border-bottom-right-radius: var(--clr-global-borderradius, 0.15rem); + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + overflow: hidden; + display: block; + -webkit-box-flex: 2; + -ms-flex-positive: 2; + flex-grow: 2; + width: 66%; } .datagrid-detail-pane-content { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - overflow: auto; - height: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + overflow: auto; + height: 100%; } .datagrid-detail-body { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - padding: 0 1.2rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 0 1.2rem; } .datagrid-detail-header { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - font-size: 0.9rem; - line-height: 1.68rem; - padding-left: 1.2rem; - margin-top: 0; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + font-size: 0.9rem; + line-height: 1.68rem; + padding-left: 1.2rem; + margin-top: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } .datagrid-detail-header .datagrid-detail-header-title { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - padding-top: 0.8rem; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding-top: 0.8rem; } .datagrid-detail-header .datagrid-detail-pane-close { - -webkit-box-flex: 1; - -ms-flex: 1 1 1.8rem; - flex: 1 1 1.8rem; - padding: 0; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + -webkit-box-flex: 1; + -ms-flex: 1 1 1.8rem; + flex: 1 1 1.8rem; + padding: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .datagrid-detail-header .datagrid-detail-pane-close .btn.btn-link { - color: #8c8c8c; - color: var(--clr-modal-close-color, #8c8c8c); - margin-top: 0.8rem; - margin-bottom: 0; - padding-right: 0; + color: #8c8c8c; + color: var(--clr-modal-close-color, #8c8c8c); + margin-top: 0.8rem; + margin-bottom: 0; + padding-right: 0; } .datagrid-detail-caret { - padding: 0; - text-align: center; + padding: 0; + text-align: center; } .datagrid-detail-caret.datagrid-cell { - padding: 0.3rem 0.45rem; + padding: 0.3rem 0.45rem; } .datagrid-detail-caret .datagrid-detail-caret-button { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - margin: 0; - padding: 0; - border: none; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; - background: 0 0; - cursor: pointer; - padding: 0.1rem 0.2rem 0.15rem; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + margin: 0; + padding: 0; + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: 0 0; + cursor: pointer; + padding: 0.1rem 0.2rem 0.15rem; } button.datagrid-detail-caret .datagrid-detail-caret-button { - cursor: pointer; + cursor: pointer; } .datagrid-detail-caret .datagrid-detail-caret-button.is-open { - background-color: #0072a3; - background-color: var( - --clr-datagrid-detail-caret-icon-open-bg-color, - #0072a3 - ); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); + background-color: #0072a3; + background-color: var( + --clr-datagrid-detail-caret-icon-open-bg-color, + #0072a3 + ); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); } .datagrid-detail-caret - .datagrid-detail-caret-button.is-open - .datagrid-detail-caret-icon { - color: #fff; - color: var(--clr-datagrid-detail-caret-icon-open-icon-color, #fff); + .datagrid-detail-caret-button.is-open + .datagrid-detail-caret-icon { + color: #fff; + color: var(--clr-datagrid-detail-caret-icon-open-icon-color, #fff); } .datagrid-detail-caret .datagrid-detail-caret-icon { - color: #8c8c8c; - color: var(--clr-datagrid-icon-color, #8c8c8c); - margin-top: 0.1rem; + color: #8c8c8c; + color: var(--clr-datagrid-icon-color, #8c8c8c); + margin-top: 0.1rem; } .datagrid-detail-caret .spinner { - margin-top: 0.3rem; + margin-top: 0.3rem; } .datagrid-detail-caret.datagrid-column { - padding: 0.55rem 0.6rem 0.55rem; + padding: 0.55rem 0.6rem 0.55rem; } .datagrid-detail-overlay.datagrid-detail-open .datagrid-inner-wrapper { - display: none; + display: none; } .datagrid-detail-overlay .datagrid-detail-pane { - border-left: 0.05rem solid #ccc; - border-radius: 0.15rem; + border-left: 0.05rem solid #ccc; + border-radius: 0.15rem; } @media screen and (max-width: 576px) { - .datagrid-detail-open .datagrid-inner-wrapper { - display: none; - } - .datagrid-detail-pane { - border-left: 0.05rem solid #ccc; - border-radius: 0.15rem; - } + .datagrid-detail-open .datagrid-inner-wrapper { + display: none; + } + .datagrid-detail-pane { + border-left: 0.05rem solid #ccc; + border-radius: 0.15rem; + } } .column-switch { - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - padding: 0.9rem; - background-color: #fff; - background-color: var(--clr-datagrid-popover-bg-color, #fff); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - width: 12.5rem; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - z-index: 1060; + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + padding: 0.9rem; + background-color: #fff; + background-color: var(--clr-datagrid-popover-bg-color, #fff); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + width: 12.5rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + z-index: 1060; } .column-switch .switch-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - font-weight: 400; - font-size: 0.8rem; - padding-bottom: 0.6rem; - line-height: 1.2rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + font-weight: 400; + font-size: 0.8rem; + padding-bottom: 0.6rem; + line-height: 1.2rem; } .column-switch .switch-header button { - min-width: 0.9rem; - margin: 0; - padding: 0; - color: #b3b3b3; - color: var(--clr-datagrid-column-switch-header-font-color, #b3b3b3); + min-width: 0.9rem; + margin: 0; + padding: 0; + color: #b3b3b3; + color: var(--clr-datagrid-column-switch-header-font-color, #b3b3b3); } .column-switch .switch-header button:hover { - color: #0072a3; - color: var(--clr-datagrid-column-switch-header-font-hover-color, #0072a3); + color: #0072a3; + color: var(--clr-datagrid-column-switch-header-font-hover-color, #0072a3); } .column-switch .switch-content { - max-height: 15rem; - overflow-y: auto; - min-height: 1.25rem; + max-height: 15rem; + overflow-y: auto; + min-height: 1.25rem; } .column-switch .switch-content li { - line-height: 1.2rem; - padding-left: 0.1rem; + line-height: 1.2rem; + padding-left: 0.1rem; } .column-switch .switch-footer .btn { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } .column-switch .switch-footer .action-right { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .datagrid-filter { - margin-top: 0.24rem; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - padding: 0.9rem; - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; + margin-top: 0.24rem; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + padding: 0.9rem; + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; } .datagrid-filter .datagrid-filter-close-wrapper { - text-align: right; + text-align: right; } .datagrid-filter .datagrid-filter-close-wrapper .close { - float: none; + float: none; } .datagrid-filter .datagrid-filter-apply { - margin-bottom: 0; + margin-bottom: 0; } .datagrid-action-overflow { - position: absolute; - background: #fff; - background: var(--clr-datagrid-popover-bg-color, #fff); - padding: 0.3rem; - margin-left: 0.3rem; - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-datagrid-popover-border-color, #ccc); - -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); - -webkit-box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - box-shadow: 0 0.05rem 0.15rem - var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); - border-radius: 0.15rem; - border-radius: var(--clr-global-borderradius, 0.15rem); - font-weight: 400; - white-space: nowrap; + position: absolute; + background: #fff; + background: var(--clr-datagrid-popover-bg-color, #fff); + padding: 0.3rem; + margin-left: 0.3rem; + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-datagrid-popover-border-color, #ccc); + -webkit-box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + box-shadow: 0 0.05rem 0.15rem rgba(140, 140, 140, 0.25); + -webkit-box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + box-shadow: 0 0.05rem 0.15rem + var(--clr-datagrid-popovers-box-shadow-color, rgba(140, 140, 140, 0.25)); + border-radius: 0.15rem; + border-radius: var(--clr-global-borderradius, 0.15rem); + font-weight: 400; + white-space: nowrap; } .datagrid-action-overflow::before { - content: ''; - position: absolute; - top: 50%; - right: 100%; - width: 0; - height: 0; - margin-top: -0.3rem; - border: 0.3rem solid transparent; - border-left: 0 none; - border-right-color: #ccc; - border-right-color: var(--clr-datagrid-popover-border-color, #ccc); + content: ""; + position: absolute; + top: 50%; + right: 100%; + width: 0; + height: 0; + margin-top: -0.3rem; + border: 0.3rem solid transparent; + border-left: 0 none; + border-right-color: #ccc; + border-right-color: var(--clr-datagrid-popover-border-color, #ccc); } .datagrid-action-overflow::after { - content: ''; - position: absolute; - top: 50%; - right: 100%; - width: 0; - height: 0; - margin-top: -0.25rem; - border-right: 0.25rem solid #fff; - border-top: 0.25rem solid transparent; - border-bottom: 0.25rem solid transparent; + content: ""; + position: absolute; + top: 50%; + right: 100%; + width: 0; + height: 0; + margin-top: -0.25rem; + border-right: 0.25rem solid #fff; + border-top: 0.25rem solid transparent; + border-bottom: 0.25rem solid transparent; } .datagrid-action-overflow .action-item { - color: #666; - color: var(--clr-dropdown-text-color, #666); - font-size: 0.7rem; - letter-spacing: normal; - background: 0 0; - border: 0; - cursor: pointer; - display: block; - line-height: 1.15rem; - margin: 0; - padding: 0.05rem 1.2rem 0; - text-align: left; - width: 100%; + color: #666; + color: var(--clr-dropdown-text-color, #666); + font-size: 0.7rem; + letter-spacing: normal; + background: 0 0; + border: 0; + cursor: pointer; + display: block; + line-height: 1.15rem; + margin: 0; + padding: 0.05rem 1.2rem 0; + text-align: left; + width: 100%; } .datagrid-action-overflow .action-item:focus, .datagrid-action-overflow .action-item:hover { - text-decoration: none; - background-color: #e8e8e8; - background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); + text-decoration: none; + background-color: #e8e8e8; + background-color: var(--clr-datagrid-action-popover-hover-color, #e8e8e8); } .datagrid-action-overflow .action-item.active { - background-color: #e8e8e8; - background-color: var(--clr-datagrid-row-hover, #e8e8e8); - color: #000; - color: var(--clr-datagrid-row-hover-font-color, #000); + background-color: #e8e8e8; + background-color: var(--clr-datagrid-row-hover, #e8e8e8); + color: #000; + color: var(--clr-datagrid-row-hover-font-color, #000); } .datagrid-action-overflow .action-item:focus { - outline: 0; + outline: 0; } .datagrid-action-overflow .action-item.disabled, .datagrid-action-overflow .action-item:disabled { - cursor: not-allowed; - opacity: 0.4; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: not-allowed; + opacity: 0.4; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .datagrid-action-overflow .action-item.disabled:hover, .datagrid-action-overflow .action-item:disabled:hover { - background: 0 0; + background: 0 0; } .datagrid-action-overflow .action-item.disabled:active, .datagrid-action-overflow .action-item.disabled:focus, .datagrid-action-overflow .action-item:disabled:active, .datagrid-action-overflow .action-item:disabled:focus { - background: 0 0; - -webkit-box-shadow: none; - box-shadow: none; + background: 0 0; + -webkit-box-shadow: none; + box-shadow: none; } .datagrid-action-overflow .action-item cds-icon { - vertical-align: middle; - -webkit-transform: translate3d(0, -1px, 0); - transform: translate3d(0, -1px, 0); + vertical-align: middle; + -webkit-transform: translate3d(0, -1px, 0); + transform: translate3d(0, -1px, 0); } .datagrid-host.datagrid-calculate-mode { - display: block; + display: block; } .datagrid-host.datagrid-calculate-mode .datagrid, .datagrid-host.datagrid-calculate-mode .datagrid-footer, .datagrid-host.datagrid-calculate-mode .datagrid-row-clickable, .datagrid-host.datagrid-calculate-mode .datagrid-row-master { - display: none; + display: none; } .datagrid-host.datagrid-calculate-mode .datagrid-calculation-table { - display: table; - table-layout: auto; + display: table; + table-layout: auto; } .datagrid-host.datagrid-calculate-mode - .datagrid-calculation-table - .datagrid-calculation-header { - display: table-header-group; + .datagrid-calculation-table + .datagrid-calculation-header { + display: table-header-group; } .datagrid-host.datagrid-calculate-mode - .datagrid-calculation-table - .datagrid-calculation-header - .datagrid-column { - display: table-cell; - min-width: 4.8rem; + .datagrid-calculation-table + .datagrid-calculation-header + .datagrid-column { + display: table-cell; + min-width: 4.8rem; } .datagrid-host.datagrid-calculate-mode - .datagrid-calculation-table - .datagrid-calculation-header - .datagrid-column { - border-color: #e8e8e8; - border-color: var(--clr-tablerow-bordercolor, #e8e8e8); - border-width: 0.05rem; - border-width: var(--clr-table-borderwidth, 0.05rem); - border-style: solid; - padding: 0.55rem 0.6rem 0.55rem; - vertical-align: top; - color: #666; - color: var(--clr-p6-color, #666); - font-weight: 600; - font-weight: var(--clr-font-weight-bold, 600); - font-size: 0.55rem; - letter-spacing: 0.03em; - line-height: 0.6rem; - margin-top: 1.2rem; - margin-bottom: 0; + .datagrid-calculation-table + .datagrid-calculation-header + .datagrid-column { + border-color: #e8e8e8; + border-color: var(--clr-tablerow-bordercolor, #e8e8e8); + border-width: 0.05rem; + border-width: var(--clr-table-borderwidth, 0.05rem); + border-style: solid; + padding: 0.55rem 0.6rem 0.55rem; + vertical-align: top; + color: #666; + color: var(--clr-p6-color, #666); + font-weight: 600; + font-weight: var(--clr-font-weight-bold, 600); + font-size: 0.55rem; + letter-spacing: 0.03em; + line-height: 0.6rem; + margin-top: 1.2rem; + margin-bottom: 0; } .datagrid-host.datagrid-calculate-mode - .datagrid-calculation-table - .datagrid-row { - display: table-row; + .datagrid-calculation-table + .datagrid-row { + display: table-row; } .datagrid-host.datagrid-calculate-mode - .datagrid-calculation-table - .datagrid-row - .datagrid-cell { - display: table-cell; - min-width: 4.8rem; - font-size: 0.65rem; - line-height: 0.7rem; - padding: 0.55rem 0.6rem 0.55rem; - vertical-align: top; + .datagrid-calculation-table + .datagrid-row + .datagrid-cell { + display: table-cell; + min-width: 4.8rem; + font-size: 0.65rem; + line-height: 0.7rem; + padding: 0.55rem 0.6rem 0.55rem; + vertical-align: top; } .datagrid-host.datagrid-calculate-mode .datagrid-column-separator { - display: none; + display: none; } .datagrid-host.datagrid-calculate-mode .datagrid-placeholder-container { - display: none; + display: none; } .datagrid-host.datagrid-calculate-mode .datagrid-fixed-column { - display: none; + display: none; } .fade { - opacity: 0; - -webkit-transition: opacity 0.2s ease-in-out; - transition: opacity 0.2s ease-in-out; - will-change: opacity; + opacity: 0; + -webkit-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; + will-change: opacity; } .fade.in { - opacity: 1; + opacity: 1; } .fadeDown { - opacity: 0; - -webkit-transform: translate(0, -25%); - transform: translate(0, -25%); - -webkit-transition: - opacity 0.2s ease-in-out, - -webkit-transform 0.2s ease-in-out; - transition: - opacity 0.2s ease-in-out, - -webkit-transform 0.2s ease-in-out; - transition: - opacity 0.2s ease-in-out, - transform 0.2s ease-in-out; - transition: - opacity 0.2s ease-in-out, - transform 0.2s ease-in-out, - -webkit-transform 0.2s ease-in-out; - will-change: opacity, transform; + opacity: 0; + -webkit-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: + opacity 0.2s ease-in-out, + -webkit-transform 0.2s ease-in-out; + transition: + opacity 0.2s ease-in-out, + -webkit-transform 0.2s ease-in-out; + transition: + opacity 0.2s ease-in-out, + transform 0.2s ease-in-out; + transition: + opacity 0.2s ease-in-out, + transform 0.2s ease-in-out, + -webkit-transform 0.2s ease-in-out; + will-change: opacity, transform; } .fadeDown.in { - opacity: 1; - -webkit-transform: translate(0, 0); - transform: translate(0, 0); + opacity: 1; + -webkit-transform: translate(0, 0); + transform: translate(0, 0); } .offscreen-focus-rebounder { - position: fixed !important; - border: none !important; - height: 1px !important; - width: 1px !important; - left: 0 !important; - top: -convertBaselineToBase20(1) !important; - overflow: hidden !important; - visibility: hidden !important; - padding: 0 !important; - margin: 0 0 -convertBaselineToBase20(1) 0 !important; - visibility: visible !important; + position: fixed !important; + border: none !important; + height: 1px !important; + width: 1px !important; + left: 0 !important; + top: -convertBaselineToBase20(1) !important; + overflow: hidden !important; + visibility: hidden !important; + padding: 0 !important; + margin: 0 0 -convertBaselineToBase20(1) 0 !important; + visibility: visible !important; } :root { - --clr-nav-box-shadow-color: var(--clr-color-neutral-400); - --clr-nav-active-box-shadow-color: var(--clr-color-action-600); - --clr-nav-active-bg-color: var(--clr-global-selection-color); - --clr-nav-hover-bg-color: var(--clr-sidenav-link-hover-color); - --clr-nav-link-color: var(--clr-color-neutral-600); - --clr-nav-link-active-color: var(--clr-color-neutral-1000); - --clr-nav-link-font-weight: var(--clr-p1-font-weight); - --clr-nav-link-active-font-weight: var(--clr-nav-link-font-weight); + --clr-nav-box-shadow-color: var(--clr-color-neutral-400); + --clr-nav-active-box-shadow-color: var(--clr-color-action-600); + --clr-nav-active-bg-color: var(--clr-global-selection-color); + --clr-nav-hover-bg-color: var(--clr-sidenav-link-hover-color); + --clr-nav-link-color: var(--clr-color-neutral-600); + --clr-nav-link-active-color: var(--clr-color-neutral-1000); + --clr-nav-link-font-weight: var(--clr-p1-font-weight); + --clr-nav-link-active-font-weight: var(--clr-nav-link-font-weight); } @media screen { - section[aria-hidden='true'] { - display: none; - } -} -[data-hidden='true'] { + section[aria-hidden="true"] { display: none; + } +} +[data-hidden="true"] { + display: none; } button.nav-link { - border-radius: 0; - text-transform: capitalize; - min-width: 0; + border-radius: 0; + text-transform: capitalize; + min-width: 0; } .tabs-overflow { - position: relative; + position: relative; } .tabs-overflow .nav-item { - margin-right: 0; + margin-right: 0; } .tab-content { - display: inline; + display: inline; } _:-ms-fullscreen .tab-content, :root .tab-content { - display: inline-block; - width: 100%; + display: inline-block; + width: 100%; } .tabs-vertical { - display: -webkit-box; - display: -ms-flexbox; - display: flex; + display: -webkit-box; + display: -ms-flexbox; + display: flex; } .tabs-vertical > .nav { - height: auto; - -webkit-box-shadow: none; - box-shadow: none; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - margin-right: 1.2rem; - overflow: auto; - -ms-flex-negative: 0; - flex-shrink: 0; - padding: 0.2rem; - width: 12rem; - min-width: 2.4rem; + height: auto; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + margin-right: 1.2rem; + overflow: auto; + -ms-flex-negative: 0; + flex-shrink: 0; + padding: 0.2rem; + width: 12rem; + min-width: 2.4rem; } .tabs-vertical > .nav .nav-link { - text-align: left; - padding: 0 0.6rem; - border: none; - margin-bottom: 0.05rem; - -ms-flex-negative: 0; - flex-shrink: 0; - margin-top: 0; - margin-left: 0; - width: 100%; + text-align: left; + padding: 0 0.6rem; + border: none; + margin-bottom: 0.05rem; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-top: 0; + margin-left: 0; + width: 100%; } .tabs-vertical > .nav .nav-link.nav-item { - margin-right: 0; + margin-right: 0; } .tabs-vertical > .nav .nav-link.active, .tabs-vertical > .nav .nav-link:hover { - -webkit-box-shadow: inset 0.15rem 0 0 #0072a3; - box-shadow: inset 0.15rem 0 0 #0072a3; - -webkit-box-shadow: inset 0.15rem 0 0 var(--clr-nav-active-box-shadow-color); - box-shadow: inset 0.15rem 0 0 var(--clr-nav-active-box-shadow-color); + -webkit-box-shadow: inset 0.15rem 0 0 #0072a3; + box-shadow: inset 0.15rem 0 0 #0072a3; + -webkit-box-shadow: inset 0.15rem 0 0 var(--clr-nav-active-box-shadow-color); + box-shadow: inset 0.15rem 0 0 var(--clr-nav-active-box-shadow-color); } .tabs-vertical > .nav .nav-link.active { - background-color: #d8e3e9; - background-color: var(--clr-nav-active-bg-color, #d8e3e9); -} -.tabs-vertical > .nav .nav-link:not(.active):hover { - background-color: #e8e8e8; - background-color: var(--clr-nav-hover-bg-color, #e8e8e8); -} -:root { - --clr-wizard-main-bgcolor: var(--clr-color-neutral-0); - --clr-wizard-sidenav-bgcolor: var(--clr-color-neutral-50); - --clr-wizard-main-textColor: var(--clr-color-on-neutral-0); - --clr-wizard-sidenav-text: var(--clr-color-neutral-900); - --clr-wizard-sidenav-text--active: var(--clr-global-on-selection-color); - --clr-wizard-title-text: var(--clr-color-neutral-1000); - --clr-wizard-stepnav-border-size: 0.2rem; - --clr-wizard-stepnav-border-color: var(--clr-color-neutral-200); - --clr-wizard-stepnav-border-color--active: var(--clr-color-success-400); - --clr-wizard-stepnav-active-bgcolor: var(--clr-global-selection-color); - --clr-wizard-header-action-color: var(--clr-color-neutral-600); - --clr-wizard-header-action-color--hovered: var(--clr-color-neutral-1000); - --clr-wizard-border-radius: var(--clr-global-borderradius); - --clr-wizard-stepnav-active-border-radius: var(--clr-wizard-border-radius); - --clr-wizard-step-nav-border-color: hsl(0, 0%, 89%); - --clr-wizard-box-shadow-color: rgba(0, 0, 0, 0.2); -} -.clr-wizard .modal-dialog { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); - box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem - var(--clr-wizard-box-shadow-color); - box-shadow: 0 0.05rem 0.1rem 0.1rem var(--clr-wizard-box-shadow-color); - height: 50%; - max-height: 100%; -} -.clr-wizard .modal-content { - border-radius: 0; - border-top-right-radius: 0.15rem; - border-top-right-radius: var(--clr-wizard-border-radius, 0.15rem); - border-bottom-right-radius: 0.15rem; - border-bottom-right-radius: var(--clr-wizard-border-radius, 0.15rem); - -webkit-box-shadow: none; - box-shadow: none; - padding: 0; - -webkit-box-flex: 2; - -ms-flex: 2 2 auto; - flex: 2 2 auto; - width: 66%; - height: initial; - overflow: hidden; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: start; - -ms-flex-align: start; - align-items: flex-start; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; + background-color: #d8e3e9; + background-color: var(--clr-nav-active-bg-color, #d8e3e9); +} +.tabs-vertical > .nav .nav-link:not(.active):hover { + background-color: #e8e8e8; + background-color: var(--clr-nav-hover-bg-color, #e8e8e8); +} +:root { + --clr-wizard-main-bgcolor: var(--clr-color-neutral-0); + --clr-wizard-sidenav-bgcolor: var(--clr-color-neutral-50); + --clr-wizard-main-textColor: var(--clr-color-on-neutral-0); + --clr-wizard-sidenav-text: var(--clr-color-neutral-900); + --clr-wizard-sidenav-text--active: var(--clr-global-on-selection-color); + --clr-wizard-title-text: var(--clr-color-neutral-1000); + --clr-wizard-stepnav-border-size: 0.2rem; + --clr-wizard-stepnav-border-color: var(--clr-color-neutral-200); + --clr-wizard-stepnav-border-color--active: var(--clr-color-success-400); + --clr-wizard-stepnav-active-bgcolor: var(--clr-global-selection-color); + --clr-wizard-header-action-color: var(--clr-color-neutral-600); + --clr-wizard-header-action-color--hovered: var(--clr-color-neutral-1000); + --clr-wizard-border-radius: var(--clr-global-borderradius); + --clr-wizard-stepnav-active-border-radius: var(--clr-wizard-border-radius); + --clr-wizard-step-nav-border-color: hsl(0, 0%, 89%); + --clr-wizard-box-shadow-color: rgba(0, 0, 0, 0.2); +} +.clr-wizard .modal-dialog { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); + box-shadow: 0 0.05rem 0.1rem 0.1rem rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 0.05rem 0.1rem 0.1rem var(--clr-wizard-box-shadow-color); + box-shadow: 0 0.05rem 0.1rem 0.1rem var(--clr-wizard-box-shadow-color); + height: 50%; + max-height: 100%; +} +.clr-wizard .modal-content { + border-radius: 0; + border-top-right-radius: 0.15rem; + border-top-right-radius: var(--clr-wizard-border-radius, 0.15rem); + border-bottom-right-radius: 0.15rem; + border-bottom-right-radius: var(--clr-wizard-border-radius, 0.15rem); + -webkit-box-shadow: none; + box-shadow: none; + padding: 0; + -webkit-box-flex: 2; + -ms-flex: 2 2 auto; + flex: 2 2 auto; + width: 66%; + height: initial; + overflow: hidden; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: start; + -ms-flex-align: start; + align-items: flex-start; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .clr-wizard .modal-header, .clr-wizard .modal-header--accessible { - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - width: 100%; - padding: 1.2rem 0.95rem 0.3rem 1.2rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + width: 100%; + padding: 1.2rem 0.95rem 0.3rem 1.2rem; } .clr-wizard .modal-title { - color: #000; - color: var(--clr-wizard-title-text, #000); - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - width: 100%; - line-height: 1.356rem; - margin-top: -0.156rem; + color: #000; + color: var(--clr-wizard-title-text, #000); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + width: 100%; + line-height: 1.356rem; + margin-top: -0.156rem; } .clr-wizard .modal-body { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - color: #000; - color: var(--clr-wizard-main-textColor, #000); - width: 100%; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + color: #000; + color: var(--clr-wizard-main-textColor, #000); + width: 100%; } .clr-wizard .modal-footer { - padding: 0; - display: block; - padding-top: 1.2rem; - height: 4.2rem; - min-height: 4.2rem; - max-height: 4.2rem; - width: 100%; - -webkit-box-flex: 0; - -ms-flex: 0 0 4.2rem; - flex: 0 0 4.2rem; + padding: 0; + display: block; + padding-top: 1.2rem; + height: 4.2rem; + min-height: 4.2rem; + max-height: 4.2rem; + width: 100%; + -webkit-box-flex: 0; + -ms-flex: 0 0 4.2rem; + flex: 0 0 4.2rem; } .clr-wizard .clr-wizard-btn { - margin: 0; - max-width: 100%; - display: block; + margin: 0; + max-width: 100%; + display: block; } .clr-wizard .modal-title-text { - display: inline-block; - -webkit-box-flex: 0; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - width: 100%; - outline: 0; + display: inline-block; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + width: 100%; + outline: 0; } .clr-wizard .modal-title-wrapper { - width: 100%; + width: 100%; } .clr-wizard .modal-header-actions-wrapper { - -webkit-box-flex: 1; - -ms-flex: 1 0 auto; - flex: 1 0 auto; - padding-left: 0.6rem; - padding-right: 0.2rem; + -webkit-box-flex: 1; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + padding-left: 0.6rem; + padding-right: 0.2rem; } .clr-wizard .clr-wizard-header-action { - height: 1.2rem; - width: 1.2rem; - padding: 0; - margin: 0; - min-width: 1.2rem; - line-height: 1.2rem; - font-size: 1.3rem; - color: #8c8c8c; - color: var(--clr-wizard-header-action-color, #8c8c8c); - -webkit-transition: color linear 0.2s; - transition: color linear 0.2s; + height: 1.2rem; + width: 1.2rem; + padding: 0; + margin: 0; + min-width: 1.2rem; + line-height: 1.2rem; + font-size: 1.3rem; + color: #8c8c8c; + color: var(--clr-wizard-header-action-color, #8c8c8c); + -webkit-transition: color linear 0.2s; + transition: color linear 0.2s; } .clr-wizard .clr-wizard-header-action a { - color: #8c8c8c; - color: var(--clr-wizard-header-action-color, #8c8c8c); + color: #8c8c8c; + color: var(--clr-wizard-header-action-color, #8c8c8c); } .clr-wizard .clr-wizard-header-action:active, .clr-wizard .clr-wizard-header-action:focus, .clr-wizard .clr-wizard-header-action:hover { - color: #000; - color: var(--clr-wizard-header-action-color--hovered, #000); + color: #000; + color: var(--clr-wizard-header-action-color--hovered, #000); } .clr-wizard .clr-wizard-header-action cds-icon { - height: 1.1rem; - width: 1.1rem; + height: 1.1rem; + width: 1.1rem; } .clr-wizard .clr-wizard-stepnav-wrapper { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - width: 34%; - max-width: 34%; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-ordinal-group: 0; - -ms-flex-order: -1; - order: -1; - overflow: hidden; - overflow-y: auto; - padding-bottom: 1.2rem; - line-height: 1.2rem; - border-right-width: 0.05rem; - border-right-width: var(--clr-global-borderwidth, 0.05rem); - border-right-style: solid; - border-right-color: #e3e3e3; - border-right-color: var(--clr-wizard-step-nav-border-color, #e3e3e3); - height: 100%; - background-color: #fafafa; - background-color: var(--clr-wizard-sidenav-bgcolor, #fafafa); - border-radius: 0; - border-top-left-radius: 0.15rem; - border-top-left-radius: var(--clr-wizard-border-radius, 0.15rem); - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-wizard-border-radius, 0.15rem); + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 34%; + max-width: 34%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + overflow: hidden; + overflow-y: auto; + padding-bottom: 1.2rem; + line-height: 1.2rem; + border-right-width: 0.05rem; + border-right-width: var(--clr-global-borderwidth, 0.05rem); + border-right-style: solid; + border-right-color: #e3e3e3; + border-right-color: var(--clr-wizard-step-nav-border-color, #e3e3e3); + height: 100%; + background-color: #fafafa; + background-color: var(--clr-wizard-sidenav-bgcolor, #fafafa); + border-radius: 0; + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-wizard-border-radius, 0.15rem); + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-wizard-border-radius, 0.15rem); } .clr-wizard .clr-wizard-stepnav { - padding-left: 1.2rem; - display: block; - font-size: 0.7rem; - color: #333; - color: var(--clr-wizard-sidenav-text, #333); - width: 100%; - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; + padding-left: 1.2rem; + display: block; + font-size: 0.7rem; + color: #333; + color: var(--clr-wizard-sidenav-text, #333); + width: 100%; + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; } .clr-wizard .clr-wizard-stepnav-list { - display: block; - -webkit-box-shadow: none; - box-shadow: none; - counter-reset: a; - white-space: nowrap; - height: auto; - list-style-type: none; - margin: 0; - width: 100%; + display: block; + -webkit-box-shadow: none; + box-shadow: none; + counter-reset: a; + white-space: nowrap; + height: auto; + list-style-type: none; + margin: 0; + width: 100%; } .clr-wizard .clr-wizard-stepnav-item { - display: block; - -webkit-box-shadow: 0.2rem 0 0 #e8e8e8 inset; - box-shadow: 0.2rem 0 0 #e8e8e8 inset; - -webkit-box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 - var(--clr-wizard-stepnav-border-color) inset; - box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 - var(--clr-wizard-stepnav-border-color) inset; - margin: 0 0 -0.05rem 0; - padding: 0.3rem 0; - padding-left: 0.4rem; - color: #333; - color: var(--clr-wizard-sidenav-text, #333); - font-weight: 400; - font-weight: var(--clr-font-weight-regular, 400); + display: block; + -webkit-box-shadow: 0.2rem 0 0 #e8e8e8 inset; + box-shadow: 0.2rem 0 0 #e8e8e8 inset; + -webkit-box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 + var(--clr-wizard-stepnav-border-color) inset; + box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 + var(--clr-wizard-stepnav-border-color) inset; + margin: 0 0 -0.05rem 0; + padding: 0.3rem 0; + padding-left: 0.4rem; + color: #333; + color: var(--clr-wizard-sidenav-text, #333); + font-weight: 400; + font-weight: var(--clr-font-weight-regular, 400); } .clr-wizard .clr-wizard-stepnav-item.active { - color: #000; - color: var(--clr-wizard-sidenav-text--active, #000); - font-weight: 500; - font-weight: var(--clr-font-weight-semibold, 500); + color: #000; + color: var(--clr-wizard-sidenav-text--active, #000); + font-weight: 500; + font-weight: var(--clr-font-weight-semibold, 500); } .clr-wizard .clr-wizard-stepnav-item.active .clr-wizard-stepnav-link { - background-color: #d8e3e9; - background-color: var(--clr-wizard-stepnav-active-bgcolor, #d8e3e9); - border-radius: 0; - border-top-left-radius: 0.15rem; - border-top-left-radius: var(--clr-wizard-border-radius, 0.15rem); - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-wizard-border-radius, 0.15rem); + background-color: #d8e3e9; + background-color: var(--clr-wizard-stepnav-active-bgcolor, #d8e3e9); + border-radius: 0; + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-wizard-border-radius, 0.15rem); + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-wizard-border-radius, 0.15rem); } .clr-wizard .clr-wizard-stepnav-item.complete { - -webkit-box-shadow: 0.2rem 0 0 #5eb715 inset; - box-shadow: 0.2rem 0 0 #5eb715 inset; - -webkit-box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 - var(--clr-wizard-stepnav-border-color--active) inset; - box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 - var(--clr-wizard-stepnav-border-color--active) inset; - -webkit-transition: -webkit-box-shadow 0.2s ease-in; - transition: -webkit-box-shadow 0.2s ease-in; - transition: box-shadow 0.2s ease-in; - transition: - box-shadow 0.2s ease-in, - -webkit-box-shadow 0.2s ease-in; + -webkit-box-shadow: 0.2rem 0 0 #5eb715 inset; + box-shadow: 0.2rem 0 0 #5eb715 inset; + -webkit-box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 + var(--clr-wizard-stepnav-border-color--active) inset; + box-shadow: var(--clr-wizard-stepnav-border-size) 0 0 + var(--clr-wizard-stepnav-border-color--active) inset; + -webkit-transition: -webkit-box-shadow 0.2s ease-in; + transition: -webkit-box-shadow 0.2s ease-in; + transition: box-shadow 0.2s ease-in; + transition: + box-shadow 0.2s ease-in, + -webkit-box-shadow 0.2s ease-in; } .clr-wizard .clr-wizard-stepnav-item.error { - -webkit-box-shadow: 0.2rem 0 0 #c21d00 inset; - box-shadow: 0.2rem 0 0 #c21d00 inset; - -webkit-transition: -webkit-box-shadow 0.2s ease-in; - transition: -webkit-box-shadow 0.2s ease-in; - transition: box-shadow 0.2s ease-in; - transition: - box-shadow 0.2s ease-in, - -webkit-box-shadow 0.2s ease-in; + -webkit-box-shadow: 0.2rem 0 0 #c21d00 inset; + box-shadow: 0.2rem 0 0 #c21d00 inset; + -webkit-transition: -webkit-box-shadow 0.2s ease-in; + transition: -webkit-box-shadow 0.2s ease-in; + transition: box-shadow 0.2s ease-in; + transition: + box-shadow 0.2s ease-in, + -webkit-box-shadow 0.2s ease-in; } .clr-wizard .clr-wizard-stepnav-item.no-click button { - pointer-events: none; + pointer-events: none; } .clr-wizard .clr-wizard-stepnav-link { - width: 100%; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - color: inherit; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - padding: 0 0.15rem 0 0.5rem; - font-size: 0.7rem; - font-weight: inherit; - letter-spacing: normal; - text-align: left; - text-transform: none; - margin: 0; + width: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + color: inherit; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 0 0.15rem 0 0.5rem; + font-size: 0.7rem; + font-weight: inherit; + letter-spacing: normal; + text-align: left; + text-transform: none; + margin: 0; } .clr-wizard .clr-wizard-stepnav-link .clr-wizard-stepnav-link-suffix { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - min-width: 1.2rem; - padding-right: 0.35rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + min-width: 1.2rem; + padding-right: 0.35rem; } .clr-wizard - .clr-wizard-stepnav-link - .clr-wizard-stepnav-link-suffix - .clr-wizard-stepnav-item-error-icon { - -webkit-transform: none; - transform: none; - fill: #c21d00; + .clr-wizard-stepnav-link + .clr-wizard-stepnav-link-suffix + .clr-wizard-stepnav-item-error-icon { + -webkit-transform: none; + transform: none; + fill: #c21d00; } .clr-wizard .clr-wizard-title { - color: #000; - color: var(--clr-wizard-title-text, #000); - margin-top: 0; - padding-top: 1.2rem; - padding-left: 1.2rem; - padding-right: 0.6rem; - padding-bottom: 1.2rem; - -webkit-box-flex: 0; - -ms-flex: 0 0 auto; - flex: 0 0 auto; - font-size: 1.1rem; - line-height: 1.2rem; + color: #000; + color: var(--clr-wizard-title-text, #000); + margin-top: 0; + padding-top: 1.2rem; + padding-left: 1.2rem; + padding-right: 0.6rem; + padding-bottom: 1.2rem; + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + font-size: 1.1rem; + line-height: 1.2rem; } .clr-wizard .modal-content-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-flex: 1; - -ms-flex: 1 1 100%; - flex: 1 1 100%; - height: 100%; - width: 100%; - max-height: 100%; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex: 1; + -ms-flex: 1 1 100%; + flex: 1 1 100%; + height: 100%; + width: 100%; + max-height: 100%; } .clr-wizard .modal-content-wrapper .modal-nav { - height: auto; + height: auto; } .clr-wizard .clr-wizard-footer-buttons { - text-align: right; - padding-right: 1.2rem; - margin: 0; + text-align: right; + padding-right: 1.2rem; + margin: 0; } .clr-wizard .clr-wizard-footer-buttons-wrapper { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -ms-flex-wrap: nowrap; - flex-wrap: nowrap; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; } .clr-wizard .clr-wizard-btn-wrapper { - -webkit-box-flex: 0; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - min-width: 4.2rem; - padding-left: 0.6rem; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; + min-width: 4.2rem; + padding-left: 0.6rem; } -.clr-wizard .clr-wizard-btn-wrapper[aria-hidden='true'] { - display: none; +.clr-wizard .clr-wizard-btn-wrapper[aria-hidden="true"] { + display: none; } .clr-wizard .clr-wizard-btn.btn-link { - padding: 0; + padding: 0; } .clr-wizard .clr-wizard-content { - display: block; + display: block; } -.clr-wizard .clr-wizard-page:not([aria-hidden='true']) { - padding: 1.2rem; - padding-top: 0.9rem; - display: block; +.clr-wizard .clr-wizard-page:not([aria-hidden="true"]) { + padding: 1.2rem; + padding-top: 0.9rem; + display: block; } .clr-wizard .modal-dialog { - height: 75vh; + height: 75vh; } .clr-wizard .modal-body { - max-height: 100%; + max-height: 100%; } .clr-wizard.wizard-md .modal-dialog { - min-height: 21rem; - max-height: 25.2rem; + min-height: 21rem; + max-height: 25.2rem; } .clr-wizard.wizard-md .clr-wizard-stepnav-wrapper, .clr-wizard.wizard-md .modal-content { - max-height: 25.2rem; + max-height: 25.2rem; } .clr-wizard.wizard-md .clr-wizard-stepnav-wrapper { - min-width: 10.8rem; - max-width: 12rem; + min-width: 10.8rem; + max-width: 12rem; } .clr-wizard.wizard-lg .modal-dialog { - min-height: 21rem; - max-height: 36rem; + min-height: 21rem; + max-height: 36rem; } .clr-wizard.wizard-lg .clr-wizard-stepnav-wrapper, .clr-wizard.wizard-lg .modal-content { - max-height: 36rem; + max-height: 36rem; } .clr-wizard.wizard-lg .clr-wizard-stepnav-wrapper, .clr-wizard.wizard-lg .nav-panel { - min-width: 12rem; - max-width: 14.4rem; + min-width: 12rem; + max-width: 14.4rem; } .clr-wizard.wizard-xl .modal-dialog { - height: 75vh; - max-height: none; + height: 75vh; + max-height: none; } .clr-wizard.wizard-xl .clr-wizard-stepnav-wrapper, .clr-wizard.wizard-xl .nav-panel { - min-width: 12rem; - max-width: 15.6rem; + min-width: 12rem; + max-width: 15.6rem; } .clr-wizard .spinner:not(.spinner-inline):not(.clr-treenode-spinner) { - left: calc(50% + 5.75rem); - position: absolute; - top: 40%; + left: calc(50% + 5.75rem); + position: absolute; + top: 40%; } .clr-wizard-page > :first-child { - margin-top: 0; + margin-top: 0; } .clr-wizard-page > :first-child > :first-child { - margin-top: 0; + margin-top: 0; } .clr-wizard-page > form:first-child { - padding-top: 0; + padding-top: 0; } .clr-wizard-page > form:first-child > .form-block:first-child { - margin-top: 0; + margin-top: 0; } .clr-wizard--inline { - display: block; - width: 100%; + display: block; + width: 100%; } .clr-wizard--inline > clr-modal > .modal:focus { - outline-style: none; - outline-color: transparent; + outline-style: none; + outline-color: transparent; } .clr-wizard--inline clr-modal { - height: 100%; - width: 100%; - display: block; + height: 100%; + width: 100%; + display: block; } .clr-wizard--inline .modal { - padding: 0; - position: static; - height: 100%; - max-height: 100%; + padding: 0; + position: static; + height: 100%; + max-height: 100%; } .clr-wizard--inline .modal .content-container { - height: 100%; + height: 100%; } .clr-wizard--inline .modal .content-container .nav-panel { - height: 99%; - width: 99%; + height: 99%; + width: 99%; } .clr-wizard--inline .modal .modal-content { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .clr-wizard--inline .modal .modal-dialog { - min-height: 100%; - height: 100%; - width: 100%; - z-index: auto; + min-height: 100%; + height: 100%; + width: 100%; + z-index: auto; } .clr-wizard--inline .modal-body { - height: 100%; + height: 100%; } .clr-wizard--inline .modal-header .close, .clr-wizard--inline .modal-header--accessible .close { - display: none; + display: none; } .clr-wizard--inline .nav.navList { - padding-top: 0; + padding-top: 0; } .clr-wizard--inline .modal-dialog .modal-content .modal-body .content-area { - overflow-y: auto; + overflow-y: auto; } .clr-wizard--inline .modal-backdrop { - height: 0; - width: 0; - display: none; + height: 0; + width: 0; + display: none; } .clr-wizard--inline .modal-content-wrapper { - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - height: 100%; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + height: 100%; } .clr-wizard--inline .clr-wizard-stepnav-wrapper, .clr-wizard--inline.clr-wizard .modal-content { - min-height: 100%; - height: auto; - max-height: 100%; + min-height: 100%; + height: auto; + max-height: 100%; } .clr-wizard--inline .clr-wizard-stepnav-wrapper .clr-wizard-stepnav, .clr-wizard--inline.clr-wizard .modal-content .clr-wizard-stepnav { - height: 100%; + height: 100%; } .clr-wizard--no-shadow .modal-content-wrapper, .clr-wizard--no-shadow .modal-dialog { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .clr-wizard--no-title .clr-wizard-title { - display: none; + display: none; } .clr-wizard--no-title .clr-wizard-stepnav { - padding-top: 1.2rem; + padding-top: 1.2rem; } @media screen { - .clr-wizard-page[aria-hidden='true'] { - display: none; - } + .clr-wizard-page[aria-hidden="true"] { + display: none; + } } @supports (-ms-ime-align: auto) { - .clr-wizard .clr-wizard-header-action { - margin-top: -0.15rem; - } + .clr-wizard .clr-wizard-header-action { + margin-top: -0.15rem; + } } :root { - --clr-signpost-content-bg-color: var(--clr-color-neutral-0); - --clr-signpost-content-border-color: var(--clr-color-neutral-500); - --clr-signpost-action-color: var(--clr-color-neutral-600); - --clr-signpost-action-hover-color: var(--clr-color-action-700); - --clr-signpost-border-radius: var(--clr-global-borderradius); + --clr-signpost-content-bg-color: var(--clr-color-neutral-0); + --clr-signpost-content-border-color: var(--clr-color-neutral-500); + --clr-signpost-action-color: var(--clr-color-neutral-600); + --clr-signpost-action-hover-color: var(--clr-color-action-700); + --clr-signpost-border-radius: var(--clr-global-borderradius); } .signpost { - display: inline-block; + display: inline-block; } .signpost:hover { - cursor: pointer; + cursor: pointer; } .signpost .signpost-action { - min-width: 1.2rem; - margin: 0; - padding: 0; - color: #8c8c8c; - color: var(--clr-signpost-action-color, #8c8c8c); + min-width: 1.2rem; + margin: 0; + padding: 0; + color: #8c8c8c; + color: var(--clr-signpost-action-color, #8c8c8c); } .signpost .signpost-action cds-icon { - height: 1.2rem; - width: 1.2rem; + height: 1.2rem; + width: 1.2rem; } .signpost .signpost-action.active, .signpost .signpost-action:hover { - color: #00648f; - color: var(--clr-signpost-action-hover-color, #00648f); + color: #00648f; + color: var(--clr-signpost-action-hover-color, #00648f); } .signpost .signpost-content-header button cds-icon { - height: 0.8rem; - width: 0.8rem; + height: 0.8rem; + width: 0.8rem; } .signpost-trigger { - margin: 0; - padding: 0; - display: inline-block; + margin: 0; + padding: 0; + display: inline-block; } .signpost-content { - background-color: transparent; - min-width: 10.8rem; - max-width: 18rem; - min-height: 2.4rem; - max-height: 25.2rem; - display: inline-block; - position: relative; - z-index: 1070; + background-color: transparent; + min-width: 10.8rem; + max-width: 18rem; + min-height: 2.4rem; + max-height: 25.2rem; + display: inline-block; + position: relative; + z-index: 1070; } .signpost-content:hover { - cursor: default; + cursor: default; } .signpost-content .popover-pointer { - height: 0; - width: 0; - position: absolute; + height: 0; + width: 0; + position: absolute; } .signpost-content .popover-pointer:before { - content: ''; - height: 0; - width: 0; - position: absolute; + content: ""; + height: 0; + width: 0; + position: absolute; } .signpost-content.top-left .popover-pointer, .signpost-content.top-middle .popover-pointer, .signpost-content.top-right .popover-pointer { - border-top: 0.6rem solid #b3b3b3; - bottom: -0.6rem; + border-top: 0.6rem solid #b3b3b3; + bottom: -0.6rem; } .signpost-content.top-left .popover-pointer:before, .signpost-content.top-middle .popover-pointer:before, .signpost-content.top-right .popover-pointer:before { - border-top: 0.6rem solid #fff; - bottom: 0.1rem; + border-top: 0.6rem solid #fff; + bottom: 0.1rem; } .signpost-content.top-left .signpost-wrap { - border-bottom-right-radius: 0; + border-bottom-right-radius: 0; } .signpost-content.top-left .popover-pointer { - border-left: 0.6rem solid transparent; - right: -0.05rem; + border-left: 0.6rem solid transparent; + right: -0.05rem; } .signpost-content.top-left .popover-pointer:before { - border-left: 0.6rem solid transparent; - right: 0.05rem; + border-left: 0.6rem solid transparent; + right: 0.05rem; } .signpost-content.top-middle .popover-pointer { - border-right: 0.6rem solid transparent; - left: 50%; + border-right: 0.6rem solid transparent; + left: 50%; } .signpost-content.top-middle .popover-pointer:before { - border-right: 0.6rem solid transparent; - left: 0.05rem; + border-right: 0.6rem solid transparent; + left: 0.05rem; } .signpost-content.top-right .signpost-wrap { - border-bottom-left-radius: 0; + border-bottom-left-radius: 0; } .signpost-content.top-right .popover-pointer { - border-right: 0.6rem solid transparent; - left: -0.05rem; + border-right: 0.6rem solid transparent; + left: -0.05rem; } .signpost-content.top-right .popover-pointer:before { - border-right: 0.6rem solid transparent; - left: 0.05rem; + border-right: 0.6rem solid transparent; + left: 0.05rem; } .signpost-content.bottom-left .popover-pointer, .signpost-content.bottom-middle .popover-pointer, .signpost-content.bottom-right .popover-pointer { - border-bottom: 0.6rem solid #b3b3b3; - top: -0.55rem; + border-bottom: 0.6rem solid #b3b3b3; + top: -0.55rem; } .signpost-content.bottom-left .popover-pointer:before, .signpost-content.bottom-middle .popover-pointer:before, .signpost-content.bottom-right .popover-pointer:before { - border-bottom: 0.6rem solid #fff; - top: 0.1rem; + border-bottom: 0.6rem solid #fff; + top: 0.1rem; } .signpost-content.bottom-left .signpost-wrap { - border-top-right-radius: 0; + border-top-right-radius: 0; } .signpost-content.bottom-left .popover-pointer { - border-left: 0.6rem solid transparent; - right: -0.05rem; + border-left: 0.6rem solid transparent; + right: -0.05rem; } .signpost-content.bottom-left .popover-pointer:before { - border-left: 0.6rem solid transparent; - right: 0.05rem; + border-left: 0.6rem solid transparent; + right: 0.05rem; } .signpost-content.bottom-middle .popover-pointer { - border-right: 0.6rem solid transparent; - right: 50%; + border-right: 0.6rem solid transparent; + right: 50%; } .signpost-content.bottom-middle .popover-pointer:before { - border-right: 0.6rem solid transparent; - right: -0.65rem; + border-right: 0.6rem solid transparent; + right: -0.65rem; } .signpost-content.bottom-right .signpost-wrap { - border-top-left-radius: 0; + border-top-left-radius: 0; } .signpost-content.bottom-right .popover-pointer { - border-right: 0.6rem solid transparent; - left: -0.05rem; + border-right: 0.6rem solid transparent; + left: -0.05rem; } .signpost-content.bottom-right .popover-pointer:before { - border-right: 0.6rem solid transparent; - left: 0.05rem; + border-right: 0.6rem solid transparent; + left: 0.05rem; } .signpost-content.left-bottom .popover-pointer, .signpost-content.left-middle .popover-pointer, .signpost-content.left-top .popover-pointer { - border-left: 0.6rem solid #b3b3b3; - right: -0.6rem; + border-left: 0.6rem solid #b3b3b3; + right: -0.6rem; } .signpost-content.left-bottom .popover-pointer:before, .signpost-content.left-middle .popover-pointer:before, .signpost-content.left-top .popover-pointer:before { - border-left: 0.6rem solid #fff; + border-left: 0.6rem solid #fff; } .signpost-content.left-top .signpost-wrap { - border-bottom-right-radius: 0; + border-bottom-right-radius: 0; } .signpost-content.left-top .popover-pointer { - border-top: 0.6rem solid transparent; - bottom: -0.05rem; + border-top: 0.6rem solid transparent; + bottom: -0.05rem; } .signpost-content.left-top .popover-pointer:before { - border-top: 0.6rem solid transparent; - top: -0.65rem; - right: 0.1rem; + border-top: 0.6rem solid transparent; + top: -0.65rem; + right: 0.1rem; } .signpost-content.left-middle .popover-pointer { - border-bottom: 0.6rem solid transparent; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); + border-bottom: 0.6rem solid transparent; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); } .signpost-content.left-middle .popover-pointer:before { - border-bottom: 0.6rem solid transparent; - top: 0.05rem; - left: -0.7rem; + border-bottom: 0.6rem solid transparent; + top: 0.05rem; + left: -0.7rem; } .signpost-content.left-bottom .signpost-wrap { - border-top-right-radius: 0; + border-top-right-radius: 0; } .signpost-content.left-bottom .popover-pointer { - border-bottom: 0.6rem solid transparent; - top: -0.05rem; + border-bottom: 0.6rem solid transparent; + top: -0.05rem; } .signpost-content.left-bottom .popover-pointer:before { - border-bottom: 0.6rem solid transparent; - top: 0.05rem; - left: -0.7rem; + border-bottom: 0.6rem solid transparent; + top: 0.05rem; + left: -0.7rem; } .signpost-content.right-bottom .popover-pointer, .signpost-content.right-middle .popover-pointer, .signpost-content.right-top .popover-pointer { - border-right: 0.6rem solid #b3b3b3; - left: -0.6rem; + border-right: 0.6rem solid #b3b3b3; + left: -0.6rem; } .signpost-content.right-bottom .popover-pointer:before, .signpost-content.right-middle .popover-pointer:before, .signpost-content.right-top .popover-pointer:before { - border-right: 0.6rem solid #fff; - left: 0.1rem; + border-right: 0.6rem solid #fff; + left: 0.1rem; } .signpost-content.right-top .signpost-wrap { - border-bottom-left-radius: 0; + border-bottom-left-radius: 0; } .signpost-content.right-top .popover-pointer { - border-top: 0.6rem solid transparent; - bottom: -0.05rem; + border-top: 0.6rem solid transparent; + bottom: -0.05rem; } .signpost-content.right-top .popover-pointer:before { - border-top: 0.6rem solid transparent; - top: -0.65rem; + border-top: 0.6rem solid transparent; + top: -0.65rem; } .signpost-content.right-middle .popover-pointer { - border-bottom: 0.6rem solid transparent; - top: 50%; - -webkit-transform: translateY(-50%); - transform: translateY(-50%); + border-bottom: 0.6rem solid transparent; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); } .signpost-content.right-middle .popover-pointer:before { - border-bottom: 0.6rem solid transparent; - top: 0.05rem; + border-bottom: 0.6rem solid transparent; + top: 0.05rem; } .signpost-content.right-bottom .signpost-wrap { - border-top-left-radius: 0; + border-top-left-radius: 0; } .signpost-content.right-bottom .popover-pointer { - border-bottom: 0.6rem solid transparent; - top: -0.05rem; + border-bottom: 0.6rem solid transparent; + top: -0.05rem; } .signpost-content.right-bottom .popover-pointer:before { - border-bottom: 0.6rem solid transparent; - top: 0.05rem; + border-bottom: 0.6rem solid transparent; + top: 0.05rem; } .signpost-content-header { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - position: absolute; - width: 100%; - background-color: inherit; - top: 0; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end; + position: absolute; + width: 100%; + background-color: inherit; + top: 0; } .signpost-wrap { - border-radius: 0.15rem; - border-radius: var(--clr-signpost-border-radius, 0.15rem); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #b3b3b3; - border-color: var(--clr-signpost-content-border-color, #b3b3b3); - background-color: #fff; - background-color: var(--clr-signpost-content-bg-color, #fff); - z-index: 1070; - position: relative; + border-radius: 0.15rem; + border-radius: var(--clr-signpost-border-radius, 0.15rem); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #b3b3b3; + border-color: var(--clr-signpost-content-border-color, #b3b3b3); + background-color: #fff; + background-color: var(--clr-signpost-content-bg-color, #fff); + z-index: 1070; + position: relative; } .signpost-content-body { - padding: 1.2rem; - max-height: 24rem; - overflow-y: auto; + padding: 1.2rem; + max-height: 24rem; + overflow-y: auto; } .drag-handle { - cursor: -webkit-grab; - cursor: grab; + cursor: -webkit-grab; + cursor: grab; } .in-drag { - cursor: -webkit-grabbing; - cursor: grabbing; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + cursor: -webkit-grabbing; + cursor: grabbing; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .in-drag * { - pointer-events: none; + pointer-events: none; } .in-drag .draggable.being-dragged { - opacity: 0.6; + opacity: 0.6; } .draggable-ghost { - position: fixed; - display: block; - visibility: hidden; - z-index: 2147483647; - pointer-events: none; - will-change: left, top; + position: fixed; + display: block; + visibility: hidden; + z-index: 2147483647; + pointer-events: none; + will-change: left, top; } .draggable-ghost > .draggable { - margin: 0; - opacity: 1; + margin: 0; + opacity: 1; } .draggable-ghost.dropped { - opacity: 0; + opacity: 0; } .droppable.draggable-match { - border: 0.05rem dashed #c090d5; + border: 0.05rem dashed #c090d5; } .droppable.draggable-over { - border: 0.05rem dashed #680094; + border: 0.05rem dashed #680094; } :root { - --clr-accordion-text-color: var(--clr-global-font-color); - --clr-accordion-active-background-color: var(--clr-global-selection-color); - --clr-accordion-content-background-color: var(--clr-color-neutral-0); - --clr-accordion-header-background-color: var(--clr-color-neutral-50); - --clr-accordion-header-hover-background-color: var(--clr-color-neutral-200); - --clr-accordion-header-font-size: 0.8rem; - --clr-accordion-header-button-font-size: 0.6rem; - --clr-accordion-title-font-weight: 500; - --clr-accordion-title-font-size: 0.65rem; - --clr-accordion-error-color: var(--clr-global-error-color); - --clr-accordion-complete-color: var(--clr-global-success-color); - --clr-accordion-border-color: var(--clr-color-neutral-400); - --clr-accordion-border-radius: var(--clr-global-borderradius); - --clr-accordion-border-left-color: #e8e8e8; - --clr-accordion-border-left-width: 0.3rem; - --clr-accordion-border-left-color-complete: var( - --clr-accordion-complete-color - ); - --clr-accordion-border-left-color-error: var(--clr-accordion-error-color); - --clr-accordion-header-left-indicator: inset - var(--clr-accordion-border-left-width) 0 0 - var(--clr-accordion-border-left-color); - --clr-accordion-header-left-complete-indicator: inset - var(--clr-accordion-border-left-width) 0 0 - var(--clr-accordion-border-left-color-complete); - --clr-accordion-header-left-error-indicator: inset - var(--clr-accordion-border-left-width) 0 0 - var(--clr-accordion-border-left-color-error); + --clr-accordion-text-color: var(--clr-global-font-color); + --clr-accordion-active-background-color: var(--clr-global-selection-color); + --clr-accordion-content-background-color: var(--clr-color-neutral-0); + --clr-accordion-header-background-color: var(--clr-color-neutral-50); + --clr-accordion-header-hover-background-color: var(--clr-color-neutral-200); + --clr-accordion-header-font-size: 0.8rem; + --clr-accordion-header-button-font-size: 0.6rem; + --clr-accordion-title-font-weight: 500; + --clr-accordion-title-font-size: 0.65rem; + --clr-accordion-error-color: var(--clr-global-error-color); + --clr-accordion-complete-color: var(--clr-global-success-color); + --clr-accordion-border-color: var(--clr-color-neutral-400); + --clr-accordion-border-radius: var(--clr-global-borderradius); + --clr-accordion-border-left-color: #e8e8e8; + --clr-accordion-border-left-width: 0.3rem; + --clr-accordion-border-left-color-complete: var( + --clr-accordion-complete-color + ); + --clr-accordion-border-left-color-error: var(--clr-accordion-error-color); + --clr-accordion-header-left-indicator: inset + var(--clr-accordion-border-left-width) 0 0 + var(--clr-accordion-border-left-color); + --clr-accordion-header-left-complete-indicator: inset + var(--clr-accordion-border-left-width) 0 0 + var(--clr-accordion-border-left-color-complete); + --clr-accordion-header-left-error-indicator: inset + var(--clr-accordion-border-left-width) 0 0 + var(--clr-accordion-border-left-color-error); } .clr-accordion { - display: block; - counter-reset: accordion; - margin-bottom: 1.2rem; + display: block; + counter-reset: accordion; + margin-bottom: 1.2rem; } .clr-accordion-panel { - display: block; + display: block; } .clr-accordion-header { - color: #666; - color: var(--clr-accordion-text-color, #666); - border: 0.05rem solid; - border-color: #ccc; - border-color: var(--clr-accordion-border-color, #ccc); - -webkit-box-shadow: inset 0.3rem 0 0 #e8e8e8; - box-shadow: inset 0.3rem 0 0 #e8e8e8; - -webkit-box-shadow: var( - --clr-accordion-header-left-indicator, - inset 0.3rem 0 0 #e8e8e8 - ); - box-shadow: var( - --clr-accordion-header-left-indicator, - inset 0.3rem 0 0 #e8e8e8 - ); - background: #fafafa; - background: var(--clr-accordion-header-background-color, #fafafa); - -webkit-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - border-bottom: 0; - width: 100%; - font-size: 0.8rem; - font-size: var(--clr-accordion-header-font-size, 0.8rem); - text-align: left; + color: #666; + color: var(--clr-accordion-text-color, #666); + border: 0.05rem solid; + border-color: #ccc; + border-color: var(--clr-accordion-border-color, #ccc); + -webkit-box-shadow: inset 0.3rem 0 0 #e8e8e8; + box-shadow: inset 0.3rem 0 0 #e8e8e8; + -webkit-box-shadow: var( + --clr-accordion-header-left-indicator, + inset 0.3rem 0 0 #e8e8e8 + ); + box-shadow: var( + --clr-accordion-header-left-indicator, + inset 0.3rem 0 0 #e8e8e8 + ); + background: #fafafa; + background: var(--clr-accordion-header-background-color, #fafafa); + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + border-bottom: 0; + width: 100%; + font-size: 0.8rem; + font-size: var(--clr-accordion-header-font-size, 0.8rem); + text-align: left; } .clr-accordion-header:hover { - background-color: #fafafa; - background-color: var(--clr-accordion-header-background-color, #fafafa); + background-color: #fafafa; + background-color: var(--clr-accordion-header-background-color, #fafafa); } .clr-accordion-panel:last-child .clr-accordion-content, .clr-accordion-panel:last-child .clr-accordion-header { - border-bottom-style: solid; - border-bottom-width: 0.05rem; - border-bottom-width: var(--clr-global-borderwidth, 0.05rem); - border-bottom-color: #ccc; - border-bottom-color: var(--clr-accordion-border-color, #ccc); + border-bottom-style: solid; + border-bottom-width: 0.05rem; + border-bottom-width: var(--clr-global-borderwidth, 0.05rem); + border-bottom-color: #ccc; + border-bottom-color: var(--clr-accordion-border-color, #ccc); } .clr-accordion-number { - padding: 0 0.9rem; - display: none; + padding: 0 0.9rem; + display: none; } .clr-accordion-number::before { - content: counter(accordion) '.'; - counter-increment: accordion; + content: counter(accordion) "."; + counter-increment: accordion; } .clr-accordion-header-button { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row wrap; - flex-flow: row wrap; - -webkit-box-flex: 1; - -ms-flex: 1 1 0%; - flex: 1 1 0%; - width: 100%; - border: 0; - padding: 0.9rem; - background: 0 0; - text-align: left; - cursor: pointer; - color: #666; - color: var(--clr-accordion-text-color, #666); + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -webkit-box-flex: 1; + -ms-flex: 1 1 0%; + flex: 1 1 0%; + width: 100%; + border: 0; + padding: 0.9rem; + background: 0 0; + text-align: left; + cursor: pointer; + color: #666; + color: var(--clr-accordion-text-color, #666); } @media (min-width: 576px) { - .clr-accordion-header-button { - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-flow: row; - flex-flow: row; - } + .clr-accordion-header-button { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-flow: row; + flex-flow: row; + } } .clr-accordion-status { - width: 1.8rem; - display: inline-block; - vertical-align: top; + width: 1.8rem; + display: inline-block; + vertical-align: top; } .clr-accordion-title { - display: inline-block; + display: inline-block; } @media (min-width: 576px) { - clr-step-title.clr-accordion-title { - min-width: 8.64rem; - } + clr-step-title.clr-accordion-title { + min-width: 8.64rem; + } } .clr-accordion-header-has-description .clr-accordion-title { - max-width: 13.2rem; + max-width: 13.2rem; } .clr-accordion-description { - display: inline-block; - max-width: 32.5rem; + display: inline-block; + max-width: 32.5rem; } @media (min-width: 576px) { - .clr-accordion-description { - margin-left: 1.8rem; - } + .clr-accordion-description { + margin-left: 1.8rem; + } } .clr-accordion-content { - background: #fff; - background: var(--clr-accordion-content-background-color, #fff); - border-width: 0.05rem; - border-width: var(--clr-global-borderwidth, 0.05rem); - border-style: solid; - border-color: #ccc; - border-color: var(--clr-accordion-border-color, #ccc); - border-bottom: 0; - overflow: hidden; - padding: 0.9rem; - display: none; + background: #fff; + background: var(--clr-accordion-content-background-color, #fff); + border-width: 0.05rem; + border-width: var(--clr-global-borderwidth, 0.05rem); + border-style: solid; + border-color: #ccc; + border-color: var(--clr-accordion-border-color, #ccc); + border-bottom: 0; + overflow: hidden; + padding: 0.9rem; + display: none; } .clr-accordion-content .clr-form { - padding: 0; + padding: 0; } .clr-accordion-content.ng-trigger { - padding: 0; + padding: 0; } @media (min-width: 576px) { - .clr-accordion-content { - padding: 0.9rem 2.7rem; - } + .clr-accordion-content { + padding: 0.9rem 2.7rem; + } } .clr-accordion-inner-content { - padding: 0.9rem; + padding: 0.9rem; } @media (min-width: 576px) { - .clr-accordion-inner-content { - padding: 0.9rem 2.7rem; - } + .clr-accordion-inner-content { + padding: 0.9rem 2.7rem; + } } .clr-accordion-angle { - -webkit-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; } .clr-accordion-complete-icon, .clr-accordion-error-icon { - height: 1.2rem; - width: 1.2rem; - display: none; - padding: 0; - margin: 0 0.6rem; + height: 1.2rem; + width: 1.2rem; + display: none; + padding: 0; + margin: 0 0.6rem; } .clr-accordion-panel-open .clr-accordion-content { - display: block; + display: block; } .clr-accordion-panel-open .clr-accordion-angle { - visibility: visible; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); + visibility: visible; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } .clr-accordion-panel-open .clr-accordion-header { - background: #d8e3e9; - background: var(--clr-accordion-active-background-color, #d8e3e9); + background: #d8e3e9; + background: var(--clr-accordion-active-background-color, #d8e3e9); } .clr-accordion-panel-complete .clr-accordion-complete-icon { - display: inline-block; - color: #5aa220; - color: var(--clr-accordion-complete-color, #5aa220); + display: inline-block; + color: #5aa220; + color: var(--clr-accordion-complete-color, #5aa220); } .clr-accordion-panel-complete .clr-accordion-angle { - visibility: visible; + visibility: visible; } .clr-accordion-panel-complete .clr-accordion-header { - -webkit-box-shadow: inset 0.3rem 0 0 #5aa220; - box-shadow: inset 0.3rem 0 0 #5aa220; - -webkit-box-shadow: var( - --clr-accordion-header-left-complete-indicator, - inset 0.3rem 0 0 #5aa220 - ); - box-shadow: var( - --clr-accordion-header-left-complete-indicator, - inset 0.3rem 0 0 #5aa220 - ); + -webkit-box-shadow: inset 0.3rem 0 0 #5aa220; + box-shadow: inset 0.3rem 0 0 #5aa220; + -webkit-box-shadow: var( + --clr-accordion-header-left-complete-indicator, + inset 0.3rem 0 0 #5aa220 + ); + box-shadow: var( + --clr-accordion-header-left-complete-indicator, + inset 0.3rem 0 0 #5aa220 + ); } .clr-accordion-panel-complete .ng-trigger.clr-accordion-content { - display: block; + display: block; } .clr-accordion-panel-error .clr-accordion-header { - border-bottom: 0.05rem solid; - border-color: #c21d00; - border-color: var(--clr-accordion-border-left-color-error, #c21d00); - -webkit-box-shadow: inset 0.3rem 0 0 #c21d00; - box-shadow: inset 0.3rem 0 0 #c21d00; - -webkit-box-shadow: var( - --clr-accordion-header-left-error-indicator, - inset 0.3rem 0 0 #c21d00 - ); - box-shadow: var( - --clr-accordion-header-left-error-indicator, - inset 0.3rem 0 0 #c21d00 - ); - background-color: #d8e3e9; - background-color: var(--clr-accordion-active-background-color, #d8e3e9); + border-bottom: 0.05rem solid; + border-color: #c21d00; + border-color: var(--clr-accordion-border-left-color-error, #c21d00); + -webkit-box-shadow: inset 0.3rem 0 0 #c21d00; + box-shadow: inset 0.3rem 0 0 #c21d00; + -webkit-box-shadow: var( + --clr-accordion-header-left-error-indicator, + inset 0.3rem 0 0 #c21d00 + ); + box-shadow: var( + --clr-accordion-header-left-error-indicator, + inset 0.3rem 0 0 #c21d00 + ); + background-color: #d8e3e9; + background-color: var(--clr-accordion-active-background-color, #d8e3e9); } .clr-accordion-panel-error .clr-accordion-error-icon { - display: inline-block; - color: #c21d00; - color: var(--clr-accordion-error-color, #c21d00); + display: inline-block; + color: #c21d00; + color: var(--clr-accordion-error-color, #c21d00); } .clr-accordion-panel-complete .clr-accordion-number, .clr-accordion-panel-error .clr-accordion-number { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); - clip-path: inset(50%); - padding: 0; - border: 0; - height: 1px; - width: 1px; - overflow: hidden; - white-space: nowrap; - top: 0; - left: 0; + position: absolute; + clip: rect(1px, 1px, 1px, 1px); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; + white-space: nowrap; + top: 0; + left: 0; } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-header { - -webkit-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } .clr-accordion:not(.clr-stepper-forms) - .clr-accordion-panel:first-child - .clr-accordion-header { - border-top-left-radius: 0.15rem; - border-top-left-radius: var(--clr-accordion-border-radius, 0.15rem); - border-top-right-radius: 0.15rem; - border-top-right-radius: var(--clr-accordion-border-radius, 0.15rem); + .clr-accordion-panel:first-child + .clr-accordion-header { + border-top-left-radius: 0.15rem; + border-top-left-radius: var(--clr-accordion-border-radius, 0.15rem); + border-top-right-radius: 0.15rem; + border-top-right-radius: var(--clr-accordion-border-radius, 0.15rem); } .clr-accordion:not(.clr-stepper-forms) - .clr-accordion-panel:last-child - .clr-accordion-content, + .clr-accordion-panel:last-child + .clr-accordion-content, .clr-accordion:not(.clr-stepper-forms) - .clr-accordion-panel:last-child - .clr-accordion-header { - border-bottom-left-radius: 0.15rem; - border-bottom-left-radius: var(--clr-accordion-border-radius, 0.15rem); - border-bottom-right-radius: 0.15rem; - border-bottom-right-radius: var(--clr-accordion-border-radius, 0.15rem); + .clr-accordion-panel:last-child + .clr-accordion-header { + border-bottom-left-radius: 0.15rem; + border-bottom-left-radius: var(--clr-accordion-border-radius, 0.15rem); + border-bottom-right-radius: 0.15rem; + border-bottom-right-radius: var(--clr-accordion-border-radius, 0.15rem); } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-title { - font-weight: 500; - font-weight: var(--clr-accordion-title-font-weight, 500); - font-size: 0.65rem; - font-size: var(--clr-accordion-title-font-size, 0.65rem); + font-weight: 500; + font-weight: var(--clr-accordion-title-font-weight, 500); + font-size: 0.65rem; + font-size: var(--clr-accordion-title-font-size, 0.65rem); } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-header-button { - font-size: 0.6rem; - font-size: var(--clr-accordion-header-button-font-size, 0.6rem); - padding: 0.3rem 0.6rem; + font-size: 0.6rem; + font-size: var(--clr-accordion-header-button-font-size, 0.6rem); + padding: 0.3rem 0.6rem; } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-content { - padding: 0; + padding: 0; } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-inner-content { - padding: 0.3rem 1.75rem; + padding: 0.3rem 1.75rem; } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-status { - width: 0.96rem; + width: 0.96rem; } .clr-accordion:not(.clr-stepper-forms) .clr-accordion-status cds-icon { - height: 0.7rem; - width: 0.7rem; + height: 0.7rem; + width: 0.7rem; } .clr-accordion-panel-open .clr-accordion-header { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-bottom: 0 !important; + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + border-bottom: 0 !important; } .clr-stepper-forms .clr-accordion-panel-inactive .clr-accordion-angle { - visibility: hidden; + visibility: hidden; } .clr-stepper-forms .clr-accordion-panel-open .clr-accordion-angle { - visibility: visible; + visibility: visible; } .clr-stepper-forms .clr-accordion-status { - min-width: 3.6rem; + min-width: 3.6rem; } .clr-stepper-forms .clr-accordion-number { - display: inline-block; + display: inline-block; } .clr-step-button { - display: block; - margin-top: 1.2rem; + display: block; + margin-top: 1.2rem; } :root { - --clr-timeline-line-color: var(--clr-color-neutral-500); - --clr-timeline-step-header-color: var(--clr-color-neutral-600); - --clr-timeline-step-title-color: var(--clr-color-neutral-700); - --clr-timeline-step-description-color: var(--clr-color-neutral-700); - --clr-timeline-incomplete-step-color: var(--clr-color-neutral-600); - --clr-timeline-current-step-color: var(--clr-color-action-600); - --clr-timeline-success-step-color: var(--clr-color-success-400); - --clr-timeline-error-step-color: var(--clr-color-danger-800); - --clr-timeline-step-title-font-weight: var(--clr-p2-font-weight); + --clr-timeline-line-color: var(--clr-color-neutral-500); + --clr-timeline-step-header-color: var(--clr-color-neutral-600); + --clr-timeline-step-title-color: var(--clr-color-neutral-700); + --clr-timeline-step-description-color: var(--clr-color-neutral-700); + --clr-timeline-incomplete-step-color: var(--clr-color-neutral-600); + --clr-timeline-current-step-color: var(--clr-color-action-600); + --clr-timeline-success-step-color: var(--clr-color-success-400); + --clr-timeline-error-step-color: var(--clr-color-danger-800); + --clr-timeline-step-title-font-weight: var(--clr-p2-font-weight); } .clr-timeline { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - padding: 0.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 0.6rem; } .clr-timeline-step { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - width: 100%; - min-width: 8.75rem; - margin-left: 0.6rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + width: 100%; + min-width: 8.75rem; + margin-left: 0.6rem; } .clr-timeline-step cds-icon { - height: 1.8rem; - width: 1.8rem; - min-height: 1.8rem; - min-width: 1.8rem; + height: 1.8rem; + width: 1.8rem; + min-height: 1.8rem; + min-width: 1.8rem; } -.clr-timeline-step cds-icon[shape='circle'] { - color: #8c8c8c; - color: var(--clr-timeline-incomplete-step-color, #8c8c8c); +.clr-timeline-step cds-icon[shape="circle"] { + color: #8c8c8c; + color: var(--clr-timeline-incomplete-step-color, #8c8c8c); } -.clr-timeline-step cds-icon[shape='dot-circle'] { - color: #0072a3; - color: var(--clr-timeline-current-step-color, #0072a3); +.clr-timeline-step cds-icon[shape="dot-circle"] { + color: #0072a3; + color: var(--clr-timeline-current-step-color, #0072a3); } -.clr-timeline-step cds-icon[shape='success-standard'] { - color: #5eb715; - color: var(--clr-timeline-success-step-color, #5eb715); +.clr-timeline-step cds-icon[shape="success-standard"] { + color: #5eb715; + color: var(--clr-timeline-success-step-color, #5eb715); } -.clr-timeline-step cds-icon[shape='error-standard'] { - color: #c21d00; - color: var(--clr-timeline-error-step-color, #c21d00); +.clr-timeline-step cds-icon[shape="error-standard"] { + color: #c21d00; + color: var(--clr-timeline-error-step-color, #c21d00); } .clr-timeline-step:not(:last-of-type) .clr-timeline-step-body::before { - content: ''; - background: #b3b3b3; - background: var(--clr-timeline-line-color, #b3b3b3); - height: 0.1rem; - width: calc(100% - 0.9rem - 0.1rem); - -webkit-transform: translate(1.7rem, -0.95rem); - transform: translate(1.7rem, -0.95rem); + content: ""; + background: #b3b3b3; + background: var(--clr-timeline-line-color, #b3b3b3); + height: 0.1rem; + width: calc(100% - 0.9rem - 0.1rem); + -webkit-transform: translate(1.7rem, -0.95rem); + transform: translate(1.7rem, -0.95rem); } .clr-timeline-step-header { - color: #8c8c8c; - color: var(--clr-timeline-step-header-color, #8c8c8c); - font-size: 0.65rem; - line-height: 0.9rem; - white-space: nowrap; - margin-bottom: 0.4rem; + color: #8c8c8c; + color: var(--clr-timeline-step-header-color, #8c8c8c); + font-size: 0.65rem; + line-height: 0.9rem; + white-space: nowrap; + margin-bottom: 0.4rem; } .clr-timeline-step-body { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .clr-timeline-step-title { - color: #666; - color: var(--clr-timeline-step-title-color, #666); - font-size: 0.65rem; - font-weight: 500; - font-weight: var(--clr-timeline-step-title-font-weight, 500); - line-height: 0.9rem; - margin-top: 0.4rem; - margin-bottom: 0.3rem; + color: #666; + color: var(--clr-timeline-step-title-color, #666); + font-size: 0.65rem; + font-weight: 500; + font-weight: var(--clr-timeline-step-title-font-weight, 500); + line-height: 0.9rem; + margin-top: 0.4rem; + margin-bottom: 0.3rem; } .clr-timeline-step-description { - color: #666; - color: var(--clr-timeline-step-description-color, #666); - font-size: 0.55rem; - line-height: 0.8rem; + color: #666; + color: var(--clr-timeline-step-description-color, #666); + font-size: 0.55rem; + line-height: 0.8rem; } .clr-timeline-step-description button { - display: block; - margin-top: 0.4rem; + display: block; + margin-top: 0.4rem; } .clr-timeline-step-description img { - width: 100%; - margin-top: 0.4rem; + width: 100%; + margin-top: 0.4rem; } .clr-timeline.clr-timeline-vertical { - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - min-width: 16rem; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 16rem; } .clr-timeline.clr-timeline-vertical .clr-timeline-step { - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - margin-left: 0; - position: relative; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + margin-left: 0; + position: relative; } .clr-timeline.clr-timeline-vertical .clr-timeline-step:not(:last-of-type) { - margin-bottom: 1.8rem; + margin-bottom: 1.8rem; } .clr-timeline.clr-timeline-vertical - .clr-timeline-step:not(:last-of-type) - .clr-timeline-step-body::before { - position: absolute; - width: 0.1rem; - height: calc(100% + 0.2rem); - -webkit-transform: translate(-1.55rem, 1.4rem); - transform: translate(-1.55rem, 1.4rem); + .clr-timeline-step:not(:last-of-type) + .clr-timeline-step-body::before { + position: absolute; + width: 0.1rem; + height: calc(100% + 0.2rem); + -webkit-transform: translate(-1.55rem, 1.4rem); + transform: translate(-1.55rem, 1.4rem); } .clr-timeline.clr-timeline-vertical .clr-timeline-step-header { - text-align: right; - white-space: normal; - word-break: break-word; - width: 3rem; - min-width: 3rem; - margin-right: 0.6rem; - margin-top: 0.3rem; - margin-bottom: 0; + text-align: right; + white-space: normal; + word-break: break-word; + width: 3rem; + min-width: 3rem; + margin-right: 0.6rem; + margin-top: 0.3rem; + margin-bottom: 0; } .clr-timeline.clr-timeline-vertical .clr-timeline-step-title { - margin-top: 0; + margin-top: 0; } .clr-timeline.clr-timeline-vertical .clr-timeline-step-body { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; - min-width: 8.9rem; - margin-left: 0.6rem; - margin-top: 0.3rem; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + min-width: 8.9rem; + margin-left: 0.6rem; + margin-top: 0.3rem; } @keyframes spin { - 0% { - -webkit-transform: rotate(0); - transform: rotate(0); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.prod.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.prod.ts index 3ac914df05..83f9d9fe45 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.prod.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.prod.ts @@ -4,5 +4,5 @@ */ export const environment = { - production: true + production: true, }; diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.ts index 8afa40d215..cde05deda0 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/environments/environment.ts @@ -8,7 +8,7 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, }; /* @@ -18,4 +18,4 @@ export const environment = { * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ -import 'zone.js/plugins/zone-error'; // Included with Angular CLI. +import "zone.js/plugins/zone-error"; // Included with Angular CLI. diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/index.html b/projects/frontend/data-pipelines/gui/projects/ui/src/index.html index 725e8728ab..8a3cc9f068 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/index.html +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/index.html @@ -5,29 +5,25 @@ - - - Ui - - - - - - - -
    -
    -
    -

    Data Pipelines

    -
    - -
    -
    -
    -
    - + + + Ui + + + + + + + +
    +
    +
    +

    Data Pipelines

    +
    + +
    +
    +
    +
    + diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/main.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/main.ts index 38038c58bf..e6ef935c06 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/main.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/main.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode } from "@angular/core"; +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; +import { AppModule } from "./app/app.module"; +import { environment } from "./environments/environment"; if (environment.production) { - enableProdMode(); + enableProdMode(); } platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.error(err)); + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/polyfills.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/polyfills.ts index 0462f551ca..6b58babfe2 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/polyfills.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/polyfills.ts @@ -23,7 +23,7 @@ * BROWSER POLYFILLS */ -import '@webcomponents/custom-elements'; +import "@webcomponents/custom-elements"; /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags @@ -50,7 +50,7 @@ import '@webcomponents/custom-elements'; /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js'; // Included with Angular CLI. +import "zone.js"; // Included with Angular CLI. (window as any).global = window; /*************************************************************************************************** diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/styles.scss b/projects/frontend/data-pipelines/gui/projects/ui/src/styles.scss index 19c40d5350..3ce70079dc 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/styles.scss +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/styles.scss @@ -10,275 +10,275 @@ **/ :root { - --taurus-placeholder-color: var(--clr-color-neutral-500, #b3b3b3); + --taurus-placeholder-color: var(--clr-color-neutral-500, #b3b3b3); } /* _normalize overwrite */ ::-webkit-input-placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } /* Internet Explorer 10-11 */ :-ms-input-placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } /* Microsoft Edge */ ::-ms-input-placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } /* Chrome, Firefox, Opera, Safari 10.1+ */ ::placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } html { - scroll-behavior: smooth; + scroll-behavior: smooth; } .content-container.switch-btn-bottom .nav-content { - margin-bottom: 1.5rem; + margin-bottom: 1.5rem; } /* combo box */ .clr-popover-content { - margin-top: 10px; + margin-top: 10px; } /* vmw search component */ -vdk-search .search-container cds-icon[shape='search'] { - top: 2px !important; +vdk-search .search-container cds-icon[shape="search"] { + top: 2px !important; } /* forms */ .destination-modal-body .custom-header-item .custom-header-input { - display: inline-block; - max-width: 10rem; + display: inline-block; + max-width: 10rem; } .modal-submit { - position: absolute; - left: -9999px; - z-index: -9999; - visibility: hidden; + position: absolute; + left: -9999px; + z-index: -9999; + visibility: hidden; } vdk-form-section-container.p-header-section .form-header { - padding-bottom: 0 !important; - margin-top: 0; + padding-bottom: 0 !important; + margin-top: 0; } vdk-form-section-container.p-header-section .form-header .section-title { - font-size: 0.7rem; - font-weight: 500; + font-size: 0.7rem; + font-weight: 500; } vdk-form-section-container.p-header-section .vmw-vertical-line { - margin-top: 0 !important; + margin-top: 0 !important; } vdk-form-section-container.p-header-section .csp-edit-button { - margin-left: 1rem !important; + margin-left: 1rem !important; } /* links */ a.disabled { - pointer-events: none; - cursor: default; - opacity: 0.5; + pointer-events: none; + cursor: default; + opacity: 0.5; } /* theme */ .toogle-theme { - position: absolute; - bottom: 0; - background-color: transparent !important; - padding-left: 0 !important; + position: absolute; + bottom: 0; + background-color: transparent !important; + padding-left: 0 !important; } .toogle-theme:hover { - background-color: transparent !important; + background-color: transparent !important; } .toogle-theme .nav-link { - background-color: transparent !important; + background-color: transparent !important; } .page-title { - margin-top: 0; + margin-top: 0; } .tc { - text-align: center !important; + text-align: center !important; } /* margins */ .m-0 { - margin: 0 !important; + margin: 0 !important; } .mt-0 { - margin-top: 0 !important; + margin-top: 0 !important; } .mb-12 { - margin-bottom: 12px !important; + margin-bottom: 12px !important; } .pl-35 { - padding-left: 35px !important; + padding-left: 35px !important; } .w-100 { - width: 100% !important; + width: 100% !important; } /* modals */ .modal-body > :first-child { - margin-top: 0; + margin-top: 0; } input.modal-form-control { - width: 12rem; + width: 12rem; } select.modal-form-control { - width: 8rem; + width: 8rem; } .modal-actions { - margin-top: -20px !important; + margin-top: -20px !important; } /* color styles */ .red { - color: #eb5757; + color: #eb5757; } /* position */ .right { - float: right; + float: right; } .left { - float: left; + float: left; } /* lists */ ul.idented { - text-indent: 2em; + text-indent: 2em; } /* media */ @media (max-width: 600px) { - .right { - float: none !important; - } + .right { + float: none !important; + } } /* datagrid cells */ .cell-img-container { - min-width: 30px !important; - width: 60px !important; - text-align: center !important; + min-width: 30px !important; + width: 60px !important; + text-align: center !important; } .cell-center { - margin: auto !important; + margin: auto !important; } .enabled-column { - width: 6rem !important; + width: 6rem !important; } /* limit-height */ .limit-height { - height: 50vh; + height: 50vh; } .limit-height clr-datagrid { - height: 50vh !important; + height: 50vh !important; } .limit-height clr-datagrid .datagrid { - margin-top: 0; + margin-top: 0; } .limit-height clr-datagrid clr-dg-cell { - margin: auto; + margin: auto; } /* team-list-component */ .team-list-container .clr-form-control { - margin-top: 0 !important; + margin-top: 0 !important; } /* CSP-HEADER */ /* Hide header assets */ .header .header-actions .nav-icon.app-switcher { - width: 0; + width: 0; } .header .header-actions .nav-icon.app-switcher cds-icon { - display: none !important; + display: none !important; } .header .header-actions .nav-icon.user-menu span.org-name { - display: none; + display: none; } @media (min-width: 768px) { - .header .header-actions .nav-icon.user-menu div { - margin-top: 0.5rem; - } + .header .header-actions .nav-icon.user-menu div { + margin-top: 0.5rem; + } } .cdk-overlay-container { - z-index: 1100; + z-index: 1100; } .vdk-root__spinner-container { - --vdk-spinner-background-color: white; - - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + --vdk-spinner-background-color: white; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--vdk-spinner-background-color); + z-index: 10001; + + .checking-user { + display: table; width: 100%; - height: 100%; - background-color: var(--vdk-spinner-background-color); - z-index: 10001; - - .checking-user { - display: table; - width: 100%; - margin-top: 200px; - - .loading-spinner { - text-align: center; - display: table-cell; - vertical-align: middle; - - .loading-title { - margin-top: 0; - margin-bottom: 20px; - } - } + margin-top: 200px; + + .loading-spinner { + text-align: center; + display: table-cell; + vertical-align: middle; + + .loading-title { + margin-top: 0; + margin-bottom: 20px; + } } + } } .fade-to-dark { - &.dark { - --taurus-placeholder-color: #b3b3b3; + &.dark { + --taurus-placeholder-color: #b3b3b3; - .vdk-root__spinner-container { - --vdk-spinner-background-color: #21333b; - } + .vdk-root__spinner-container { + --vdk-spinner-background-color: #21333b; } + } } diff --git a/projects/frontend/data-pipelines/gui/projects/ui/src/test.ts b/projects/frontend/data-pipelines/gui/projects/ui/src/test.ts index e9a56292bf..fe05472e50 100644 --- a/projects/frontend/data-pipelines/gui/projects/ui/src/test.ts +++ b/projects/frontend/data-pipelines/gui/projects/ui/src/test.ts @@ -5,26 +5,33 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; +import "zone.js/testing"; +import { getTestBed } from "@angular/core/testing"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; declare const require: { - context( - path: string, - deep?: boolean, - filter?: RegExp - ): { - keys(): string[]; - (id: string): T; - }; + context( + path: string, + deep?: boolean, + filter?: RegExp, + ): { + keys(): string[]; + (id: string): T; + }; }; // First, initialize the Angular testing environment. -getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { - teardown: { destroyAfterEach: false } -}); +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false }, + }, +); // Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); +const context = require.context("./", true, /\.spec\.ts$/); // And load the modules. context.keys().map(context); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js b/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js index 643de610c2..a2ff5164d1 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js @@ -7,55 +7,68 @@ // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-junit-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma')], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser + config.set({ + basePath: "", + frameworks: ["jasmine", "@angular-devkit/build-angular"], + plugins: [ + require("karma-jasmine"), + require("karma-chrome-launcher"), + require("karma-jasmine-html-reporter"), + require("karma-junit-reporter"), + require("karma-coverage"), + require("@angular-devkit/build-angular/plugins/karma"), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + coverageReporter: { + dir: require("path").join( + __dirname, + "../../reports/coverage/documentation-ui", + ), + subdir: ".", + reporters: [ + //Code coverage - output only in HTML file + { type: "html" }, + { type: "text-summary" }, + { type: "lcovonly" }, + ], + check: { + global: { + lines: 20, }, - coverageReporter: { - dir: require('path').join(__dirname, '../../reports/coverage/documentation-ui'), - subdir: '.', - reporters: [ - //Code coverage - output only in HTML file - { type: 'html' }, - { type: 'text-summary' }, - { type: 'lcovonly' } - ], - check: { - global: { - lines: 20 - } - } - }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - junitReporter: { - outputDir: require('path').join(__dirname, '../../reports/test-results/documentation-ui'), - outputFile: 'unit-tests.xml', - useBrowserName: false - }, - reporters: ['progress', 'junit', 'coverage'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['ChromeHeadless'], - customLaunchers: { - ChromeHeadless_No_Sandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - }, - singleRun: false, - restartOnFileChange: true - }); + }, + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + junitReporter: { + outputDir: require("path").join( + __dirname, + "../../reports/test-results/documentation-ui", + ), + outputFile: "unit-tests.xml", + useBrowserName: false, + }, + reporters: ["progress", "junit", "coverage"], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ["ChromeHeadless"], + customLaunchers: { + ChromeHeadless_No_Sandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + singleRun: false, + restartOnFileChange: true, + }); }; diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts index 948b865c5c..7398537f7c 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts @@ -3,25 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; const routes: Routes = [ - { - path: '', - redirectTo: 'overview', - pathMatch: 'full' - }, - // Overview - { - path: 'overview', - loadChildren: () => import('./core/core.module').then((m) => m.CoreModule) - }, - { path: '**', redirectTo: 'overview' } + { + path: "", + redirectTo: "overview", + pathMatch: "full", + }, + // Overview + { + path: "overview", + loadChildren: () => import("./core/core.module").then((m) => m.CoreModule), + }, + { path: "**", redirectTo: "overview" }, ]; @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], }) export class AppRoutingModule {} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html index 44699a55e2..c9f809616c 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html @@ -4,65 +4,63 @@ -->
    - -
    - -
    - - - -
    - Logout -
    -
    -
    -
    -
    - -
    - - - - Overview - -
    Components
    -
    -
    + +
    + +
    + + + +
    + Logout +
    +
    +
    +
    +
    + +
    + + + + Overview + +
    Components
    +
    +
    - -
    -
    - -
    -
    + +
    +
    + +
    +
    -
    -

    - Loading Shared Components Documentation UIs -

    - -
    +
    +

    Loading Shared Components Documentation UIs

    + +
    diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss index 82fd34f4cc..9d8a8666b1 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss @@ -4,36 +4,36 @@ */ .main-container { - .content-container { - .content-area { - .csp-content-area-margin { - margin: 0 30px; - padding-bottom: 30px; - height: 100%; - } - } + .content-container { + .content-area { + .csp-content-area-margin { + margin: 0 30px; + padding-bottom: 30px; + height: 100%; + } } + } } ::ng-deep .table-documentation { - &.table th, - &.table td { - text-align: left; - } + &.table th, + &.table td { + text-align: left; + } } .checking-user { - display: table; - height: 26.5rem; - width: 100%; + display: table; + height: 26.5rem; + width: 100%; - .loading-spinner { - text-align: center; - display: table-cell; - vertical-align: middle; + .loading-spinner { + text-align: center; + display: table-cell; + vertical-align: middle; - .loading-title { - margin-bottom: 0.6rem; - } + .loading-title { + margin-bottom: 0.6rem; } + } } diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts index c12a05f7e4..445232eb0c 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts @@ -3,32 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { HttpClientTestingModule } from "@angular/common/http/testing"; -import { OAuthModule } from 'angular-oauth2-oidc'; +import { OAuthModule } from "angular-oauth2-oidc"; -import { AppComponent } from './app.component'; +import { AppComponent } from "./app.component"; -describe('AppComponent', () => { - let component: AppComponent; - let fixture: ComponentFixture; +describe("AppComponent", () => { + let component: AppComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [AppComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [OAuthModule.forRoot(), HttpClientTestingModule] - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(AppComponent); - component = fixture.debugElement.componentInstance; - }); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [OAuthModule.forRoot(), HttpClientTestingModule], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(AppComponent); + component = fixture.debugElement.componentInstance; + }); + }); - it('should create the app', () => { - expect(component).toBeTruthy(); - }); + it("should create the app", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts index 007f05a9fc..1c9f241c5e 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts @@ -3,37 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; -import { OAuthService } from 'angular-oauth2-oidc'; +import { OAuthService } from "angular-oauth2-oidc"; -import { authCodeFlowConfig } from './auth'; +import { authCodeFlowConfig } from "./auth"; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.scss"], }) export class AppComponent { - constructor(private oauthService: OAuthService) { - this.oauthService.configure(authCodeFlowConfig); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.oauthService.loadDiscoveryDocumentAndLogin(); - } - - logout(): void { - this.oauthService.logOut(); - } - - get idToken(): string { - return this.oauthService.getIdToken(); - } - - get userName(): string { - return this.oauthService.getIdentityClaims() ? this.getIdentityClaim('username') : 'N/A'; - } - - private getIdentityClaim(userNamePropName: string): string { - return this.oauthService.getIdentityClaims()[userNamePropName] as string; - } + constructor(private oauthService: OAuthService) { + this.oauthService.configure(authCodeFlowConfig); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.oauthService.loadDiscoveryDocumentAndLogin(); + } + + logout(): void { + this.oauthService.logOut(); + } + + get idToken(): string { + return this.oauthService.getIdToken(); + } + + get userName(): string { + return this.oauthService.getIdentityClaims() + ? this.getIdentityClaim("username") + : "N/A"; + } + + private getIdentityClaim(userNamePropName: string): string { + return this.oauthService.getIdentityClaims()[userNamePropName] as string; + } } diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts index a054146d6a..6c45b3877b 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts @@ -3,66 +3,71 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule } from "@angular/core"; +import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http"; +import { BrowserModule } from "@angular/platform-browser"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; +import { AuthConfig, OAuthModule, OAuthStorage } from "angular-oauth2-oidc"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { VdkSharedCoreModule, VdkSharedFeaturesModule, VdkSharedNgRxModule, VdkSharedComponentsModule } from '@versatiledatakit/shared'; +import { + VdkSharedCoreModule, + VdkSharedFeaturesModule, + VdkSharedNgRxModule, + VdkSharedComponentsModule, +} from "@versatiledatakit/shared"; -import { AuthorizationInterceptor } from './http.interceptor'; +import { AuthorizationInterceptor } from "./http.interceptor"; -import { authCodeFlowConfig } from './auth'; +import { authCodeFlowConfig } from "./auth"; -import { AppRoutingModule } from './app-routing.module'; +import { AppRoutingModule } from "./app-routing.module"; -import { AppComponent } from './app.component'; +import { AppComponent } from "./app.component"; @NgModule({ - imports: [ - AppRoutingModule, - BrowserModule, - ClarityModule, - BrowserAnimationsModule, - VdkSharedComponentsModule.forRoot(), - VdkSharedCoreModule.forRoot(), - VdkSharedFeaturesModule.forRoot({ - warning: { - serviceRequestUrl: '#' - }, - placeholder: { - serviceRequestUrl: '#' - } - }), - VdkSharedNgRxModule.forRootWithDevtools(), - HttpClientModule, - OAuthModule.forRoot({ - resourceServer: { - allowedUrls: [authCodeFlowConfig.issuer, '/metadata'], - sendAccessToken: true - } - }) - ], - declarations: [AppComponent], - providers: [ - { - provide: OAuthStorage, - useValue: localStorage - }, - { - provide: AuthConfig, - useValue: authCodeFlowConfig - }, - { - provide: HTTP_INTERCEPTORS, - useClass: AuthorizationInterceptor, - multi: true - } - ], - bootstrap: [AppComponent] + imports: [ + AppRoutingModule, + BrowserModule, + ClarityModule, + BrowserAnimationsModule, + VdkSharedComponentsModule.forRoot(), + VdkSharedCoreModule.forRoot(), + VdkSharedFeaturesModule.forRoot({ + warning: { + serviceRequestUrl: "#", + }, + placeholder: { + serviceRequestUrl: "#", + }, + }), + VdkSharedNgRxModule.forRootWithDevtools(), + HttpClientModule, + OAuthModule.forRoot({ + resourceServer: { + allowedUrls: [authCodeFlowConfig.issuer, "/metadata"], + sendAccessToken: true, + }, + }), + ], + declarations: [AppComponent], + providers: [ + { + provide: OAuthStorage, + useValue: localStorage, + }, + { + provide: AuthConfig, + useValue: authCodeFlowConfig, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthorizationInterceptor, + multi: true, + }, + ], + bootstrap: [AppComponent], }) export class AppModule {} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts index af544ea643..822476b342 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts @@ -3,24 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AuthConfig } from 'angular-oauth2-oidc'; +import { AuthConfig } from "angular-oauth2-oidc"; export const authCodeFlowConfig: AuthConfig = { - issuer: 'https://console-stg.cloud.vmware.com/csp/gateway/am/api/', - redirectUri: window.location.origin + '/index.html', - skipIssuerCheck: true, - requestAccessToken: true, - oidc: true, - strictDiscoveryDocumentValidation: false, - clientId: 'Lt44bhN5yMowdEHuxO3v1SBDKsS3aXW4GcJ', - responseType: 'code', - scope: 'openid ALL_PERMISSIONS customer_number group_names', - showDebugInformation: true, - silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', - useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes - silentRefreshTimeout: 5000, // For faster testing - timeoutFactor: 0.25, // For faster testing - sessionChecksEnabled: true, - clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, - nonceStateSeparator: 'semicolon' // Real semicolon gets mangled by IdentityServer's URI encoding + issuer: "https://console-stg.cloud.vmware.com/csp/gateway/am/api/", + redirectUri: window.location.origin + "/index.html", + skipIssuerCheck: true, + requestAccessToken: true, + oidc: true, + strictDiscoveryDocumentValidation: false, + clientId: "Lt44bhN5yMowdEHuxO3v1SBDKsS3aXW4GcJ", + responseType: "code", + scope: "openid ALL_PERMISSIONS customer_number group_names", + showDebugInformation: true, + silentRefreshRedirectUri: window.location.origin + "/silent-refresh.html", + useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes + silentRefreshTimeout: 5000, // For faster testing + timeoutFactor: 0.25, // For faster testing + sessionChecksEnabled: true, + clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, + nonceStateSeparator: "semicolon", // Real semicolon gets mangled by IdentityServer's URI encoding }; diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html index 81bdb5946c..0629a10fc1 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html @@ -4,8 +4,8 @@ -->
    -

    Shared Components Overview

    +

    Shared Components Overview

    -
    -
    +
    +
    diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss index 375a1d3ee4..e779fabee4 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss @@ -4,17 +4,17 @@ */ .core-component { - .card { - height: 100%; + .card { + height: 100%; - .card-block { - display: flex; - justify-content: center; - align-items: center; + .card-block { + display: flex; + justify-content: center; + align-items: center; - img { - width: 100%; - } - } + img { + width: 100%; + } } + } } diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts index 928e8aa922..b0eb087d40 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts @@ -3,29 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { CoreComponent } from './core.component'; +import { CoreComponent } from "./core.component"; -describe('CoreComponent', () => { - let component: CoreComponent; - let fixture: ComponentFixture; +describe("CoreComponent", () => { + let component: CoreComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [CoreComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CoreComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(CoreComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(CoreComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts index bc57cd4edf..138aa1ccc8 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; +import { Component } from "@angular/core"; @Component({ - selector: 'app-core', - templateUrl: './core.component.html', - styleUrls: ['./core.component.scss'] + selector: "app-core", + templateUrl: "./core.component.html", + styleUrls: ["./core.component.scss"], }) export class CoreComponent {} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts index 4b366ac6d4..cf82104f61 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts @@ -3,23 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule, Routes } from '@angular/router'; +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule, Routes } from "@angular/router"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { CoreComponent } from './core.component'; +import { CoreComponent } from "./core.component"; const routes: Routes = [ - { - path: '', - component: CoreComponent - } + { + path: "", + component: CoreComponent, + }, ]; @NgModule({ - declarations: [CoreComponent], - imports: [RouterModule.forChild(routes), CommonModule, ClarityModule] + declarations: [CoreComponent], + imports: [RouterModule.forChild(routes), CommonModule, ClarityModule], }) export class CoreModule {} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts index 164ae7bbad..30c1b08c1e 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts @@ -3,54 +3,71 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable, Optional } from '@angular/core'; -import { OAuthModuleConfig, OAuthResourceServerErrorHandler, OAuthStorage } from 'angular-oauth2-oidc'; -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable, Optional } from "@angular/core"; +import { + OAuthModuleConfig, + OAuthResourceServerErrorHandler, + OAuthStorage, +} from "angular-oauth2-oidc"; +import { + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from "@angular/common/http"; -import { Observable } from 'rxjs'; -import { authCodeFlowConfig } from './auth'; +import { Observable } from "rxjs"; +import { authCodeFlowConfig } from "./auth"; @Injectable() export class AuthorizationInterceptor implements HttpInterceptor { - constructor( - private authStorage: OAuthStorage, - private errorHandler: OAuthResourceServerErrorHandler, - @Optional() private moduleConfig: OAuthModuleConfig - ) {} - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public intercept(req: HttpRequest, next: HttpHandler): Observable> { - const url = req.url.toLowerCase(); - if (!this.moduleConfig) { - return next.handle(req); - } - if (!this.moduleConfig.resourceServer) { - return next.handle(req); - } - if (!this.moduleConfig.resourceServer.allowedUrls) { - return next.handle(req); - } - if (!this.checkUrl(url)) { - return next.handle(req); - } - - const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; - - if (sendAccessToken && url.startsWith(authCodeFlowConfig.issuer)) { - const headers = req.headers.set('Authorization', 'Basic ' + btoa(authCodeFlowConfig.clientId + ':')); - req = req.clone({ headers }); - } else if (sendAccessToken) { - const token = this.authStorage.getItem('access_token'); - const header = 'Bearer ' + token; - const headers = req.headers.set('Authorization', header); - req = req.clone({ headers }); - } - - return next.handle(req); + constructor( + private authStorage: OAuthStorage, + private errorHandler: OAuthResourceServerErrorHandler, + @Optional() private moduleConfig: OAuthModuleConfig, + ) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { + const url = req.url.toLowerCase(); + if (!this.moduleConfig) { + return next.handle(req); + } + if (!this.moduleConfig.resourceServer) { + return next.handle(req); + } + if (!this.moduleConfig.resourceServer.allowedUrls) { + return next.handle(req); + } + if (!this.checkUrl(url)) { + return next.handle(req); } - private checkUrl(url: string): boolean { - const found = this.moduleConfig.resourceServer.allowedUrls.find((u) => url.startsWith(u)); - return !!found; + const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; + + if (sendAccessToken && url.startsWith(authCodeFlowConfig.issuer)) { + const headers = req.headers.set( + "Authorization", + "Basic " + btoa(authCodeFlowConfig.clientId + ":"), + ); + req = req.clone({ headers }); + } else if (sendAccessToken) { + const token = this.authStorage.getItem("access_token"); + const header = "Bearer " + token; + const headers = req.headers.set("Authorization", header); + req = req.clone({ headers }); } + + return next.handle(req); + } + + private checkUrl(url: string): boolean { + const found = this.moduleConfig.resourceServer.allowedUrls.find((u) => + url.startsWith(u), + ); + return !!found; + } } diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts index 3ac914df05..83f9d9fe45 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts @@ -4,5 +4,5 @@ */ export const environment = { - production: true + production: true, }; diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts index 8afa40d215..cde05deda0 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts @@ -8,7 +8,7 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, }; /* @@ -18,4 +18,4 @@ export const environment = { * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ -import 'zone.js/plugins/zone-error'; // Included with Angular CLI. +import "zone.js/plugins/zone-error"; // Included with Angular CLI. diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html b/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html index 865e00f3ce..80f06a19a3 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html @@ -5,19 +5,15 @@ - - - DocumentationUi - - - - - - - - + + + DocumentationUi + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts index 38038c58bf..e6ef935c06 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode } from "@angular/core"; +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; +import { AppModule } from "./app/app.module"; +import { environment } from "./environments/environment"; if (environment.production) { - enableProdMode(); + enableProdMode(); } platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch((err) => console.error(err)); + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts index 0462f551ca..6b58babfe2 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts @@ -23,7 +23,7 @@ * BROWSER POLYFILLS */ -import '@webcomponents/custom-elements'; +import "@webcomponents/custom-elements"; /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags @@ -50,7 +50,7 @@ import '@webcomponents/custom-elements'; /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js'; // Included with Angular CLI. +import "zone.js"; // Included with Angular CLI. (window as any).global = window; /*************************************************************************************************** diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss b/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss index baebe41927..7f836bedc7 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss @@ -10,291 +10,291 @@ **/ :root { - --taurus-placeholder-color: var(--clr-color-neutral-500, #b3b3b3); + --taurus-placeholder-color: var(--clr-color-neutral-500, #b3b3b3); } /* _normalize overwrite */ ::-webkit-input-placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } /* Internet Explorer 10-11 */ :-ms-input-placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } /* Microsoft Edge */ ::-ms-input-placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } /* Chrome, Firefox, Opera, Safari 10.1+ */ ::placeholder { - /* clr-forms-placeholder-color */ - color: var(--taurus-placeholder-color) !important; + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; } .fade-to-dark.dark { - --taurus-placeholder-color: #b3b3b3; + --taurus-placeholder-color: #b3b3b3; } html { - scroll-behavior: smooth; + scroll-behavior: smooth; } .content-container.switch-btn-bottom .nav-content { - margin-bottom: 1.5rem; + margin-bottom: 1.5rem; } /* combo box */ .clr-popover-content { - margin-top: 10px; + margin-top: 10px; } /* vmw search component */ -vdk-search .search-container cds-icon[shape='search'] { - top: 2px !important; +vdk-search .search-container cds-icon[shape="search"] { + top: 2px !important; } /* forms */ .destination-modal-body .custom-header-item .custom-header-input { - display: inline-block; - max-width: 10rem; + display: inline-block; + max-width: 10rem; } .modal-submit { - position: absolute; - left: -9999px; - z-index: -9999; - visibility: hidden; + position: absolute; + left: -9999px; + z-index: -9999; + visibility: hidden; } vdk-form-section-container.p-header-section .form-header { - padding-bottom: 0 !important; - margin-top: 0; + padding-bottom: 0 !important; + margin-top: 0; } vdk-form-section-container.p-header-section .form-header .section-title { - font-size: 0.7rem; - font-weight: 500; + font-size: 0.7rem; + font-weight: 500; } vdk-form-section-container.p-header-section .vdk-vertical-line { - margin-top: 0 !important; + margin-top: 0 !important; } vdk-form-section-container.p-header-section .csp-edit-button { - margin-left: 1rem !important; + margin-left: 1rem !important; } /* links */ a.disabled { - pointer-events: none; - cursor: default; - opacity: 0.5; + pointer-events: none; + cursor: default; + opacity: 0.5; } /* theme */ .toogle-theme { - position: absolute; - bottom: 0; - background-color: transparent !important; - padding-left: 0 !important; + position: absolute; + bottom: 0; + background-color: transparent !important; + padding-left: 0 !important; } .toogle-theme:hover { - background-color: transparent !important; + background-color: transparent !important; } .toogle-theme .nav-link { - background-color: transparent !important; + background-color: transparent !important; } .page-title { - margin-top: 0; + margin-top: 0; } .tc { - text-align: center !important; + text-align: center !important; } /* margins */ .m-0 { - margin: 0 !important; + margin: 0 !important; } .mt-0 { - margin-top: 0 !important; + margin-top: 0 !important; } .mb-12 { - margin-bottom: 12px !important; + margin-bottom: 12px !important; } .pl-35 { - padding-left: 35px !important; + padding-left: 35px !important; } .w-100 { - width: 100% !important; + width: 100% !important; } /* modals */ .modal-body > :first-child { - margin-top: 0; + margin-top: 0; } input.modal-form-control { - width: 12rem; + width: 12rem; } select.modal-form-control { - width: 8rem; + width: 8rem; } .modal-actions { - margin-top: -20px !important; + margin-top: -20px !important; } /* color styles */ .red { - color: #eb5757; + color: #eb5757; } /* position */ .right { - float: right; + float: right; } .left { - float: left; + float: left; } /* lists */ ul.idented { - text-indent: 2em; + text-indent: 2em; } /* media */ @media (max-width: 600px) { - .right { - float: none !important; - } + .right { + float: none !important; + } } /* datagrid cells */ .cell-img-container { - min-width: 30px !important; - width: 60px !important; - text-align: center !important; + min-width: 30px !important; + width: 60px !important; + text-align: center !important; } .cell-center { - margin: auto !important; + margin: auto !important; } .enabled-column { - width: 6rem !important; + width: 6rem !important; } /* limit-height */ .limit-height { - height: 50vh; + height: 50vh; } .limit-height clr-datagrid { - height: 50vh !important; + height: 50vh !important; } .limit-height clr-datagrid .datagrid { - margin-top: 0; + margin-top: 0; } .limit-height clr-datagrid clr-dg-cell { - margin: auto; + margin: auto; } /* team-list-component */ .team-list-container .clr-form-control { - margin-top: 0 !important; + margin-top: 0 !important; } /* CSP-HEADER */ /* Hide header assets */ .header .header-actions .nav-icon.app-switcher { - width: 0; + width: 0; } .header .header-actions .nav-icon.app-switcher cds-icon { - display: none !important; + display: none !important; } .header .header-actions .nav-icon.user-menu span.org-name { - display: none; + display: none; } @media (min-width: 768px) { - .header .header-actions .nav-icon.user-menu div { - margin-top: 0.5rem; - } + .header .header-actions .nav-icon.user-menu div { + margin-top: 0.5rem; + } } .clr-vertical-nav .nav-group-trigger .nav-group-trigger-icon { - height: 0.8rem; + height: 0.8rem; } .page-wrapper { - padding-bottom: 2.4rem; + padding-bottom: 2.4rem; } pre { - padding: 0.5rem; - overflow: auto; + padding: 0.5rem; + overflow: auto; } p code { - color: #c21d00; + color: #c21d00; } .table-documentation { - caption { - display: none; - } + caption { + display: none; + } } .token { - &.attr-name, - &.builtin, - &.char &.inserted &.selector &.string { - color: #aa5d00; - } - - &.atrule, - &.attr-value, - &.function { - color: #aa5d00; - } - - &.constant, - &.deleted, - &.property, - &.symbol, - &.tag { - color: #007faa; - } - - &.punctuation { - color: #545454; - } - - &.class { - color: #306b00; - } - - &.access { - color: #00567a; - } + &.attr-name, + &.builtin, + &.char &.inserted &.selector &.string { + color: #aa5d00; + } + + &.atrule, + &.attr-value, + &.function { + color: #aa5d00; + } + + &.constant, + &.deleted, + &.property, + &.symbol, + &.tag { + color: #007faa; + } + + &.punctuation { + color: #545454; + } + + &.class { + color: #306b00; + } + + &.access { + color: #00567a; + } } diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts index e9a56292bf..fe05472e50 100644 --- a/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts @@ -5,26 +5,33 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; +import "zone.js/testing"; +import { getTestBed } from "@angular/core/testing"; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from "@angular/platform-browser-dynamic/testing"; declare const require: { - context( - path: string, - deep?: boolean, - filter?: RegExp - ): { - keys(): string[]; - (id: string): T; - }; + context( + path: string, + deep?: boolean, + filter?: RegExp, + ): { + keys(): string[]; + (id: string): T; + }; }; // First, initialize the Angular testing environment. -getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { - teardown: { destroyAfterEach: false } -}); +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false }, + }, +); // Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); +const context = require.context("./", true, /\.spec\.ts$/); // And load the modules. context.keys().map(context); diff --git a/projects/frontend/shared-components/gui/projects/shared/karma.conf.js b/projects/frontend/shared-components/gui/projects/shared/karma.conf.js index bdf1e1c78f..912546ecc9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/karma.conf.js +++ b/projects/frontend/shared-components/gui/projects/shared/karma.conf.js @@ -7,55 +7,65 @@ // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-junit-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma')], - client: { - jasmine: { - // you can add configuration options for Jasmine here - // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html - // for example, you can disable the random execution with `random: false` - // or set a specific seed with `seed: 4321` - }, - clearContext: false // leave Jasmine Spec Runner output visible in browser + config.set({ + basePath: "", + frameworks: ["jasmine", "@angular-devkit/build-angular"], + plugins: [ + require("karma-jasmine"), + require("karma-chrome-launcher"), + require("karma-jasmine-html-reporter"), + require("karma-junit-reporter"), + require("karma-coverage"), + require("@angular-devkit/build-angular/plugins/karma"), + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true, // removes the duplicated traces + }, + coverageReporter: { + dir: require("path").join(__dirname, "../../reports/coverage/shared"), + subdir: ".", + reporters: [ + //Code coverage - output in HTML file and Console(to be parsed in the CI/CD badge) + { type: "html" }, + { type: "text-summary" }, + { type: "lcovonly" }, + ], + check: { + global: { + lines: 80, }, - jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../reports/coverage/shared'), - subdir: '.', - reporters: [ - //Code coverage - output in HTML file and Console(to be parsed in the CI/CD badge) - { type: 'html' }, - { type: 'text-summary' }, - { type: 'lcovonly' } - ], - check: { - global: { - lines: 80 - } - } - }, - reporters: ['progress', 'junit', 'coverage'], - junitReporter: { - outputDir: require('path').join(__dirname, '../../reports/test-results/shared'), - outputFile: 'unit-tests.xml', - useBrowserName: false - }, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['ChromeHeadless'], - customLaunchers: { - ChromeHeadless_No_Sandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - }, - singleRun: false, - restartOnFileChange: true - }); + }, + }, + reporters: ["progress", "junit", "coverage"], + junitReporter: { + outputDir: require("path").join( + __dirname, + "../../reports/test-results/shared", + ), + outputFile: "unit-tests.xml", + useBrowserName: false, + }, + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ["ChromeHeadless"], + customLaunchers: { + ChromeHeadless_No_Sandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + singleRun: false, + restartOnFileChange: true, + }); }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/and.criteria.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/and.criteria.ts index 6ac9cf801c..3b2029609e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/and.criteria.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/and.criteria.ts @@ -3,38 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Criteria } from '../../interfaces'; +import { Criteria } from "../../interfaces"; /** * ** And criteria that filters elements in Array and remove those that does not meet all criterias. */ export class AndCriteria implements Criteria { - /** - * @inheritDoc - */ - readonly criterias: Criteria[]; + /** + * @inheritDoc + */ + readonly criterias: Criteria[]; - /** - * ** Constructor. - */ - constructor(...criterias: Criteria[]) { - this.criterias = criterias; - } - - /** - * @inheritDoc - */ - meetCriteria(elements: T[]): T[] { - let elementsMeetCriteria: T[] = [...elements]; + /** + * ** Constructor. + */ + constructor(...criterias: Criteria[]) { + this.criterias = criterias; + } - for (const criteria of this.criterias) { - elementsMeetCriteria = criteria.meetCriteria(elementsMeetCriteria); + /** + * @inheritDoc + */ + meetCriteria(elements: T[]): T[] { + let elementsMeetCriteria: T[] = [...elements]; - if (elementsMeetCriteria.length === 0) { - break; - } - } + for (const criteria of this.criterias) { + elementsMeetCriteria = criteria.meetCriteria(elementsMeetCriteria); - return elementsMeetCriteria; + if (elementsMeetCriteria.length === 0) { + break; + } } + + return elementsMeetCriteria; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/index.ts index e56926b993..e3512b1003 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './and.criteria'; -export * from './or.criteria'; +export * from "./and.criteria"; +export * from "./or.criteria"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/or.criteria.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/or.criteria.ts index ad3a527d3f..318a7cc4e9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/or.criteria.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/or.criteria.ts @@ -3,38 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Criteria } from '../../interfaces'; +import { Criteria } from "../../interfaces"; /** * ** Or criteria that filters elements in Array and remove those that does not meet at least one criterias. */ export class OrCriteria implements Criteria { - /** - * @inheritDoc - */ - readonly criterias: Criteria[]; + /** + * @inheritDoc + */ + readonly criterias: Criteria[]; - /** - * ** Constructor. - */ - constructor(...criterias: Criteria[]) { - this.criterias = criterias; - } + /** + * ** Constructor. + */ + constructor(...criterias: Criteria[]) { + this.criterias = criterias; + } - /** - * @inheritDoc - */ - meetCriteria(elements: T[]): T[] { - return this.criterias.reduce((elementsMeetCriteria, criteria) => { - const singleCriteriaMetElements = criteria.meetCriteria(elements); + /** + * @inheritDoc + */ + meetCriteria(elements: T[]): T[] { + return this.criterias.reduce((elementsMeetCriteria, criteria) => { + const singleCriteriaMetElements = criteria.meetCriteria(elements); - for (const element of singleCriteriaMetElements) { - if (!elementsMeetCriteria.includes(element)) { - elementsMeetCriteria.push(element); - } - } + for (const element of singleCriteriaMetElements) { + if (!elementsMeetCriteria.includes(element)) { + elementsMeetCriteria.push(element); + } + } - return elementsMeetCriteria; - }, [] as T[]); - } + return elementsMeetCriteria; + }, [] as T[]); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/index.ts index f6596f51d5..db9a40ca4f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './primitive'; -export * from './compound'; +export * from "./primitive"; +export * from "./compound"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/index.ts index b057041931..703d206098 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './primitive.criteria'; +export * from "./primitive.criteria"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/primitive.criteria.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/primitive.criteria.ts index a5505442b9..33211cae58 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/primitive.criteria.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/primitive.criteria.ts @@ -3,33 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { get } from 'lodash'; +import { get } from "lodash"; -import { Criteria } from '../../interfaces'; +import { Criteria } from "../../interfaces"; /** * ** Primitive Criteria that check equals by reference or if primitive by value, using === */ export class PrimitiveCriteria implements Criteria { - private _property: keyof T; - private _assertionValue: T[keyof T]; + private _property: keyof T; + private _assertionValue: T[keyof T]; - /** - * ** Constructor. - */ - constructor(property: keyof T, assertionValue: T[keyof T]) { - this._property = property; - this._assertionValue = assertionValue; - } + /** + * ** Constructor. + */ + constructor(property: keyof T, assertionValue: T[keyof T]) { + this._property = property; + this._assertionValue = assertionValue; + } - /** - * @inheritDoc - */ - meetCriteria(elements: T[]): T[] { - return [...(elements ?? [])].filter((element) => { - const value = get(element, this._property); + /** + * @inheritDoc + */ + meetCriteria(elements: T[]): T[] { + return [...(elements ?? [])].filter((element) => { + const value = get(element, this._property); - return value === this._assertionValue; - }); - } + return value === this._assertionValue; + }); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/public-api.ts index f6596f51d5..db9a40ca4f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/public-api.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './primitive'; -export * from './compound'; +export * from "./primitive"; +export * from "./compound"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/api-error.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/api-error.interface.ts index 7b18f686af..72e9b3454b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/api-error.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/api-error.interface.ts @@ -7,19 +7,19 @@ * ** Api error format. */ export interface ApiError { - status: number; - error: ApiErrorMessage | string; - message: string; - opId: string; - path?: string; + status: number; + error: ApiErrorMessage | string; + message: string; + opId: string; + path?: string; } /** * ** Api error message format. */ export interface ApiErrorMessage { - what: string; - why: string; - consequences?: string; - countermeasures?: string; + what: string; + why: string; + consequences?: string; + countermeasures?: string; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/index.ts index 25f0470342..cf61f1bdbc 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/api-error/model/interfaces/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './api-error.interface'; +export * from "./api-error.interface"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/index.ts index 8a0d34b512..175341b5fa 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { generateErrorCode } from './utils'; -export * from './api-error/model/interfaces'; -export * from './ui-error/model/interfaces'; -export * from './store/model/interfaces'; +export { generateErrorCode } from "./utils"; +export * from "./api-error/model/interfaces"; +export * from "./ui-error/model/interfaces"; +export * from "./store/model/interfaces"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/error-store.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/error-store.interface.ts index e983188033..b1863683c0 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/error-store.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/error-store.interface.ts @@ -3,147 +3,153 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpStatusCode } from '@angular/common/http'; +import { HttpStatusCode } from "@angular/common/http"; -import { Copy, Equals, Literal } from '../../../../interfaces'; +import { Copy, Equals, Literal } from "../../../../interfaces"; -import { ErrorRecord } from '../../../ui-error/model/interfaces'; +import { ErrorRecord } from "../../../ui-error/model/interfaces"; export type ErrorStoreChangeListener = (store: T) => void; /** * ** Interface for ErrorStore. */ -export interface ErrorStore extends Literal, Copy, Equals { - /** - * ** Error records store. - * - * - It's store for error codes (tokens), where every code is represented in string format as key part of compound object ErrorRecord. - * - Codes (tokens) should start with Class name, - * then followed by underscore and class PUBLIC_NAME, - * then followed by underscore and method name or underscore with some error specifics, - * and followed by underscore and additional details to avoid overlaps with other Class errors. - * - *
    - * pattern: - *

    - * ___ - *

    - */ - records: ErrorRecord[]; - - /** - * ** Mutation listeners invoked whenever changed occurs in records of ErrorRecord. - */ - changeListeners: Array>; - - /** - * ** Returns boolean that indicates if there is any ErrorRecords. - */ - hasErrors(): boolean; - - /** - * ** Check if store has some error code(s) by providing one or more error codes. - * - * - Comparison is executed with exact match. - * - It will return TRUE if at least one of the provided error code is found in store. - * - Evaluation is executed with operator OR for every error code. - */ - hasCode(...errorCodes: string[]): boolean; - - /** - * ** Check if store has some error code(s) by providing one or more error code patterns that will be translated to RegExp. - * - * - Comparison is executed with RegExp. - * - It will return TRUE if at least one of the provided error code patterns match error codes in store. - * - Evaluation is executed with operator OR for every error code pattern. - */ - hasCodePattern(...errorCodesPatterns: string[]): boolean; - - /** - * ** Record in store error code, object UUID and optionally actual error and Http status code if it's Http request error. - * - * - Comparison is executed with exact match. - * - Error codes are unique and distinct in context of store. - * - There could be only one error code present in the queue for given error code from one objectUUID at any moment. - */ - record(errorCode: string, objectUUID: string, error: Error, httpStatusCode?: HttpStatusCode): void; - - /** - * ** Record in store ErrorRecord. - * - * - Comparison is executed with exact match. - * - Error codes are unique and distinct in context of store. - * - There could be only one error code present in the queue for given error code from one objectUUID at any moment. - */ - record(errorRecord: ErrorRecord): void; - - /** - * ** Remove error code from store by providing one or more error codes. - * - * - Comparison is executed with exact match. - * - Error codes are unique and distinct in context of store. - * - There could be only one error code present in the queue for given error code at any moment. - */ - removeCode(...errorCodes: string[]): void; - - /** - * ** Remove error code pattern from store by providing one or more error code patterns that will be translated to RegExp. - * - * - Comparison is executed with exact RegExp. - * - Error codes are unique and distinct in context of store. - * - There could be only one error code present in the queue for given error code at any moment. - */ - removeCodePattern(...errorCodePatterns: string[]): void; - - /** - * ** Find ErrorRecord(s) for provided error code(s). - * - * - Comparison is executed with exact match. - * - Error codes are unique and distinct in context of store. - * - There could be only one error code present in the queue for given error code at any moment. - */ - findRecords(...errorCodes: string[]): ErrorRecord[]; - - /** - * ** Find ErrorRecord(s) for provided error code pattern(s) that will be translated to RegExp. - * - * - Comparison is executed with exact RegExp. - * - Error codes are unique and distinct in context of store. - * - There could be only one error code present in the queue for given error code at any moment. - */ - findRecordsByPattern(...errorCodePatterns: string[]): ErrorRecord[]; - - /** - * ** Distinct and return only error records that don't match any from provided. - */ - distinctErrorRecords(errorRecords: ErrorRecord[]): ErrorRecord[]; - - /** - * ** Purge ErrorRecords. - */ - purge(injectedStore: ErrorStore): void; - - /** - * ** Attach listener for change. - */ - onChange(callback: (store: this) => void): void; - - /** - * ** Dispose and clean store. - * - * - Useful to call before container Object get destroyed, to clean all references. - */ - dispose(): void; - - /** - * ** Clear Store. - */ - clear(): void; - - /** - * ** Make shallow copy of current Object. - * @override - */ - copy(): ErrorStore; +export interface ErrorStore + extends Literal, Copy, Equals { + /** + * ** Error records store. + * + * - It's store for error codes (tokens), where every code is represented in string format as key part of compound object ErrorRecord. + * - Codes (tokens) should start with Class name, + * then followed by underscore and class PUBLIC_NAME, + * then followed by underscore and method name or underscore with some error specifics, + * and followed by underscore and additional details to avoid overlaps with other Class errors. + * + *
    + * pattern: + *

    + * ___ + *

    + */ + records: ErrorRecord[]; + + /** + * ** Mutation listeners invoked whenever changed occurs in records of ErrorRecord. + */ + changeListeners: Array>; + + /** + * ** Returns boolean that indicates if there is any ErrorRecords. + */ + hasErrors(): boolean; + + /** + * ** Check if store has some error code(s) by providing one or more error codes. + * + * - Comparison is executed with exact match. + * - It will return TRUE if at least one of the provided error code is found in store. + * - Evaluation is executed with operator OR for every error code. + */ + hasCode(...errorCodes: string[]): boolean; + + /** + * ** Check if store has some error code(s) by providing one or more error code patterns that will be translated to RegExp. + * + * - Comparison is executed with RegExp. + * - It will return TRUE if at least one of the provided error code patterns match error codes in store. + * - Evaluation is executed with operator OR for every error code pattern. + */ + hasCodePattern(...errorCodesPatterns: string[]): boolean; + + /** + * ** Record in store error code, object UUID and optionally actual error and Http status code if it's Http request error. + * + * - Comparison is executed with exact match. + * - Error codes are unique and distinct in context of store. + * - There could be only one error code present in the queue for given error code from one objectUUID at any moment. + */ + record( + errorCode: string, + objectUUID: string, + error: Error, + httpStatusCode?: HttpStatusCode, + ): void; + + /** + * ** Record in store ErrorRecord. + * + * - Comparison is executed with exact match. + * - Error codes are unique and distinct in context of store. + * - There could be only one error code present in the queue for given error code from one objectUUID at any moment. + */ + record(errorRecord: ErrorRecord): void; + + /** + * ** Remove error code from store by providing one or more error codes. + * + * - Comparison is executed with exact match. + * - Error codes are unique and distinct in context of store. + * - There could be only one error code present in the queue for given error code at any moment. + */ + removeCode(...errorCodes: string[]): void; + + /** + * ** Remove error code pattern from store by providing one or more error code patterns that will be translated to RegExp. + * + * - Comparison is executed with exact RegExp. + * - Error codes are unique and distinct in context of store. + * - There could be only one error code present in the queue for given error code at any moment. + */ + removeCodePattern(...errorCodePatterns: string[]): void; + + /** + * ** Find ErrorRecord(s) for provided error code(s). + * + * - Comparison is executed with exact match. + * - Error codes are unique and distinct in context of store. + * - There could be only one error code present in the queue for given error code at any moment. + */ + findRecords(...errorCodes: string[]): ErrorRecord[]; + + /** + * ** Find ErrorRecord(s) for provided error code pattern(s) that will be translated to RegExp. + * + * - Comparison is executed with exact RegExp. + * - Error codes are unique and distinct in context of store. + * - There could be only one error code present in the queue for given error code at any moment. + */ + findRecordsByPattern(...errorCodePatterns: string[]): ErrorRecord[]; + + /** + * ** Distinct and return only error records that don't match any from provided. + */ + distinctErrorRecords(errorRecords: ErrorRecord[]): ErrorRecord[]; + + /** + * ** Purge ErrorRecords. + */ + purge(injectedStore: ErrorStore): void; + + /** + * ** Attach listener for change. + */ + onChange(callback: (store: this) => void): void; + + /** + * ** Dispose and clean store. + * + * - Useful to call before container Object get destroyed, to clean all references. + */ + dispose(): void; + + /** + * ** Clear Store. + */ + clear(): void; + + /** + * ** Make shallow copy of current Object. + * @override + */ + copy(): ErrorStore; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/index.ts index 3ff4da4195..f57e12fd15 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/store/model/interfaces/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './error-store.interface'; +export * from "./error-store.interface"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/index.ts index e33fc11f96..61131d29ec 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './ui-error.interface'; +export * from "./ui-error.interface"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/ui-error.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/ui-error.interface.ts index f97f3a0352..811ddb093d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/ui-error.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/ui-error/model/interfaces/ui-error.interface.ts @@ -5,137 +5,137 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/naming-convention */ -import { HttpStatusCode } from '@angular/common/http'; +import { HttpStatusCode } from "@angular/common/http"; /** * ** Error Record. */ export interface ErrorRecord { - /** - * ** Error code (token). - * - * - Code (token) should start with Class name, - * then followed by underscore and class PUBLIC_NAME, - * then followed by underscore and method name or underscore with some error specifics, - * and followed by underscore and additional details to avoid overlaps with other Class errors. - * - *
    - * pattern: - *

    - * ___ - *

    - */ - code: string; - - /** - * ** Object UUID. - */ - objectUUID: string; - - /** - * ** Actual error object. - */ - error: Error; - - /** - * ** Timestamp in milliseconds when Error is recorded in Store. - * - * - Generated using Date.now(), when record is written in store. - */ - time?: number; - - /** - * ** Http status code. - * - * - if present assume it is Http request error. - */ - httpStatusCode?: HttpStatusCode; + /** + * ** Error code (token). + * + * - Code (token) should start with Class name, + * then followed by underscore and class PUBLIC_NAME, + * then followed by underscore and method name or underscore with some error specifics, + * and followed by underscore and additional details to avoid overlaps with other Class errors. + * + *
    + * pattern: + *

    + * ___ + *

    + */ + code: string; + + /** + * ** Object UUID. + */ + objectUUID: string; + + /** + * ** Actual error object. + */ + error: Error; + + /** + * ** Timestamp in milliseconds when Error is recorded in Store. + * + * - Generated using Date.now(), when record is written in store. + */ + time?: number; + + /** + * ** Http status code. + * + * - if present assume it is Http request error. + */ + httpStatusCode?: HttpStatusCode; } /** * ** Auto generated error codes for every method of TaurusBaseApiService subclasses. */ export type ServiceHttpErrorCodes = { - /** - * ** Service method error code that match all method error codes if used as error code pattern (translated in RegExp). - */ - All: string; - - /** - * ** Service method error code that match all method error codes from group 4xx if used as error code pattern (translated in RegExp). - */ - ClientErrors: string; - - /** - * ** Service method error code for Bad request. - * - * - code: 400 - */ - BadRequest: string; - - /** - * ** Service method error code for Unauthorized. - * - * - code: 401 - */ - Unauthorized: string; - - /** - * ** Service method error code for Forbidden. - * - * - code: 403 - */ - Forbidden: string; - - /** - * ** Service method error code for Not found. - * - * - code: 404 - */ - NotFound: string; - - /** - * ** Service method error code for Method Not Allowed. - * - * - code: 405 - */ - MethodNotAllowed: string; - - /** - * ** Service method error code for Conflict. - * - * - code: 409 - */ - Conflict: string; - - /** - * ** Service method code for Unprocessable entity. - * - * - code: 422 - */ - UnprocessableEntity: string; - - /** - * ** Service method error code that match all method error codes from group 5xx if used as error code pattern (translated in RegExp). - */ - ServerErrors: string; - - /** - * ** Service method error code for Internal Server Error. - * - * - code: 500 - */ - InternalServerError: string; - - /** - * ** Service method error code for Service Unavailable. - * - * - code: 503 - */ - ServiceUnavailable: string; - - /** - * ** Service method error code for Unknown Error. - */ - Unknown: string; + /** + * ** Service method error code that match all method error codes if used as error code pattern (translated in RegExp). + */ + All: string; + + /** + * ** Service method error code that match all method error codes from group 4xx if used as error code pattern (translated in RegExp). + */ + ClientErrors: string; + + /** + * ** Service method error code for Bad request. + * + * - code: 400 + */ + BadRequest: string; + + /** + * ** Service method error code for Unauthorized. + * + * - code: 401 + */ + Unauthorized: string; + + /** + * ** Service method error code for Forbidden. + * + * - code: 403 + */ + Forbidden: string; + + /** + * ** Service method error code for Not found. + * + * - code: 404 + */ + NotFound: string; + + /** + * ** Service method error code for Method Not Allowed. + * + * - code: 405 + */ + MethodNotAllowed: string; + + /** + * ** Service method error code for Conflict. + * + * - code: 409 + */ + Conflict: string; + + /** + * ** Service method code for Unprocessable entity. + * + * - code: 422 + */ + UnprocessableEntity: string; + + /** + * ** Service method error code that match all method error codes from group 5xx if used as error code pattern (translated in RegExp). + */ + ServerErrors: string; + + /** + * ** Service method error code for Internal Server Error. + * + * - code: 500 + */ + InternalServerError: string; + + /** + * ** Service method error code for Service Unavailable. + * + * - code: 503 + */ + ServiceUnavailable: string; + + /** + * ** Service method error code for Unknown Error. + */ + Unknown: string; }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.spec.ts index 43c3c88dea..6bf7e95e3b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.spec.ts @@ -3,38 +3,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../utils'; - -import { generateErrorCode } from './error-store.utils'; - -describe('generateErrorCode', () => { - it('should verify will return correct value', () => { - // Given - const className = 'ClassName'; - const publicName = 'PublicName'; - const methodName = 'methodName'; - const additionalDetails = '500'; - - // When - const errorCode = generateErrorCode(className, publicName, methodName, additionalDetails); - - // Then - expect(errorCode).toEqual(`${className}_${publicName}_${methodName}_${additionalDetails}`); - }); - - it('should verify will generate random string if className is not provided', () => { - // Given - const className: string = null; - const publicName = 'PublicName1'; - const methodName = 'methodName1'; - const additionalDetails = '503'; - const spy = spyOn(CollectionsUtil, 'generateRandomString').and.returnValue('ClassName1'); - - // When - const errorCode = generateErrorCode(className, publicName, methodName, additionalDetails); - - // Then - expect(errorCode).toEqual(`ClassName1_${publicName}_${methodName}_${additionalDetails}`); - expect(spy).toHaveBeenCalled(); - }); +import { CollectionsUtil } from "../../../utils"; + +import { generateErrorCode } from "./error-store.utils"; + +describe("generateErrorCode", () => { + it("should verify will return correct value", () => { + // Given + const className = "ClassName"; + const publicName = "PublicName"; + const methodName = "methodName"; + const additionalDetails = "500"; + + // When + const errorCode = generateErrorCode( + className, + publicName, + methodName, + additionalDetails, + ); + + // Then + expect(errorCode).toEqual( + `${className}_${publicName}_${methodName}_${additionalDetails}`, + ); + }); + + it("should verify will generate random string if className is not provided", () => { + // Given + const className: string = null; + const publicName = "PublicName1"; + const methodName = "methodName1"; + const additionalDetails = "503"; + const spy = spyOn(CollectionsUtil, "generateRandomString").and.returnValue( + "ClassName1", + ); + + // When + const errorCode = generateErrorCode( + className, + publicName, + methodName, + additionalDetails, + ); + + // Then + expect(errorCode).toEqual( + `ClassName1_${publicName}_${methodName}_${additionalDetails}`, + ); + expect(spy).toHaveBeenCalled(); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.ts index d5434ec0a1..28ef155ff9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/error-store.utils.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpStatusCode } from '@angular/common/http'; +import { HttpStatusCode } from "@angular/common/http"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { ServiceHttpErrorCodes } from '../ui-error/model/interfaces'; +import { ServiceHttpErrorCodes } from "../ui-error/model/interfaces"; /** * ** Generates Error code (token). @@ -25,30 +25,35 @@ import { ServiceHttpErrorCodes } from '../ui-error/model/interfaces'; * ___ *

    */ -export const generateErrorCode = (className: string, classPublicName: string, methodName: string, additionalDetails: string): string => { - let errorCode = ''; +export const generateErrorCode = ( + className: string, + classPublicName: string, + methodName: string, + additionalDetails: string, +): string => { + let errorCode = ""; - if (CollectionsUtil.isString(className)) { - errorCode += `${className}`; - } else { - errorCode += CollectionsUtil.generateRandomString(); - } + if (CollectionsUtil.isString(className)) { + errorCode += `${className}`; + } else { + errorCode += CollectionsUtil.generateRandomString(); + } - if (CollectionsUtil.isString(classPublicName)) { - errorCode += `_${classPublicName}`; - } + if (CollectionsUtil.isString(classPublicName)) { + errorCode += `_${classPublicName}`; + } - if (CollectionsUtil.isString(methodName)) { - errorCode += `_${methodName}`; - } + if (CollectionsUtil.isString(methodName)) { + errorCode += `_${methodName}`; + } - if (CollectionsUtil.isString(additionalDetails)) { - errorCode += `_${additionalDetails}`; - } else { - errorCode += '_'; - } + if (CollectionsUtil.isString(additionalDetails)) { + errorCode += `_${additionalDetails}`; + } else { + errorCode += "_"; + } - return errorCode; + return errorCode; }; /** @@ -57,24 +62,88 @@ export const generateErrorCode = (className: string, classPublicName: string, me /* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */ -export const generateSupportedHttpErrorCodes = (className: string, publicName: string, method: string): ServiceHttpErrorCodes => { - const errorCodes: ServiceHttpErrorCodes = {} as ServiceHttpErrorCodes; +export const generateSupportedHttpErrorCodes = ( + className: string, + publicName: string, + method: string, +): ServiceHttpErrorCodes => { + const errorCodes: ServiceHttpErrorCodes = {} as ServiceHttpErrorCodes; - errorCodes.All = generateErrorCode(className, publicName, method, null); - errorCodes.ClientErrors = generateErrorCode(className, publicName, method, '4\\d\\d'); - errorCodes.BadRequest = generateErrorCode(className, publicName, method, `${HttpStatusCode.BadRequest}`); - errorCodes.Unauthorized = generateErrorCode(className, publicName, method, `${HttpStatusCode.Unauthorized}`); - errorCodes.Forbidden = generateErrorCode(className, publicName, method, `${HttpStatusCode.Forbidden}`); - errorCodes.NotFound = generateErrorCode(className, publicName, method, `${HttpStatusCode.NotFound}`); - errorCodes.MethodNotAllowed = generateErrorCode(className, publicName, method, `${HttpStatusCode.MethodNotAllowed}`); - errorCodes.Conflict = generateErrorCode(className, publicName, method, `${HttpStatusCode.Conflict}`); - errorCodes.UnprocessableEntity = generateErrorCode(className, publicName, method, `${HttpStatusCode.UnprocessableEntity}`); - errorCodes.ServerErrors = generateErrorCode(className, publicName, method, '5\\d\\d'); - errorCodes.InternalServerError = generateErrorCode(className, publicName, method, `${HttpStatusCode.InternalServerError}`); - errorCodes.ServiceUnavailable = generateErrorCode(className, publicName, method, `${HttpStatusCode.ServiceUnavailable}`); - errorCodes.Unknown = generateErrorCode(className, publicName, method, 'unknown'); + errorCodes.All = generateErrorCode(className, publicName, method, null); + errorCodes.ClientErrors = generateErrorCode( + className, + publicName, + method, + "4\\d\\d", + ); + errorCodes.BadRequest = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.BadRequest}`, + ); + errorCodes.Unauthorized = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.Unauthorized}`, + ); + errorCodes.Forbidden = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.Forbidden}`, + ); + errorCodes.NotFound = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.NotFound}`, + ); + errorCodes.MethodNotAllowed = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.MethodNotAllowed}`, + ); + errorCodes.Conflict = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.Conflict}`, + ); + errorCodes.UnprocessableEntity = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.UnprocessableEntity}`, + ); + errorCodes.ServerErrors = generateErrorCode( + className, + publicName, + method, + "5\\d\\d", + ); + errorCodes.InternalServerError = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.InternalServerError}`, + ); + errorCodes.ServiceUnavailable = generateErrorCode( + className, + publicName, + method, + `${HttpStatusCode.ServiceUnavailable}`, + ); + errorCodes.Unknown = generateErrorCode( + className, + publicName, + method, + "unknown", + ); - return errorCodes; + return errorCodes; }; /* eslint-enable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/index.ts index d779e40fc2..ea1cd9cfe7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/error/utils/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './error-store.utils'; +export * from "./error-store.utils"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts index 48f586477e..cd81e5ebda 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './request'; +export * from "./request"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts index d88e7dd9dd..5491b6cb79 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './request.model'; +export * from "./request.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts index dfc727b462..1a851a941b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts @@ -3,380 +3,451 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ApiPredicate, RequestFilterImpl, RequestOrderImpl, RequestPageImpl } from './request.model'; +import { + ApiPredicate, + RequestFilterImpl, + RequestOrderImpl, + RequestPageImpl, +} from "./request.model"; + +describe("RequestPage", () => { + it("should verify instance is created", () => { + // When + const instance = new RequestPageImpl(0, 0); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(RequestPageImpl); + }); + + it("should verify correct value are assigned", () => { + // Given + const pageNumber = 4; + const pageSize = 10; + + // When + const instance = new RequestPageImpl(pageNumber, pageSize); + + // Then + expect(instance.page).toEqual(pageNumber); + expect(instance.size).toEqual(pageSize); + }); + + it("should verify on Nil parameters default value will be assigned", () => { + // When + const instance = new RequestPageImpl(null, undefined); + + // Then + expect(instance.page).toEqual(1); + expect(instance.size).toEqual(25); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = RequestPageImpl.of(8, 20); + + // Then + expect(instance).toBeInstanceOf(RequestPageImpl); + expect(instance.page).toEqual(8); + expect(instance.size).toEqual(20); + }); + }); -describe('RequestPage', () => { - it('should verify instance is created', () => { - // When - const instance = new RequestPageImpl(0, 0); + describe("|empty|", () => { + it("should verify will create empty instance with default values", () => { + // When + const instance = RequestPageImpl.empty(); - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(RequestPageImpl); + // Then + expect(instance).toBeInstanceOf(RequestPageImpl); + expect(instance.page).toEqual(1); + expect(instance.size).toEqual(25); + }); + }); + + describe("|fromLiteral|", () => { + it("should verify will create new instance from given literal object", () => { + // Given + const literal = { pageNumber: 7, pageSize: 52 }; + + // When + const instance = RequestPageImpl.fromLiteral(literal); + + // Then + expect(instance).toBeInstanceOf(RequestPageImpl); + expect(instance.page).toEqual(7); + expect(instance.size).toEqual(52); + }); + }); }); + }); - it('should verify correct value are assigned', () => { + describe("Methods::", () => { + describe("|toLiteral|", () => { + it("should verify will create literal object from RequestPage object ", () => { // Given - const pageNumber = 4; - const pageSize = 10; + const requestPage = RequestPageImpl.of(5, 17); // When - const instance = new RequestPageImpl(pageNumber, pageSize); + const literal = requestPage.toLiteral(); // Then - expect(instance.page).toEqual(pageNumber); - expect(instance.size).toEqual(pageSize); + expect(literal).not.toBeInstanceOf(RequestPageImpl); + expect(literal).toBeInstanceOf(Object); + expect(literal.pageNumber).toEqual(5); + expect(literal.pageSize).toEqual(17); + }); }); - it('should verify on Nil parameters default value will be assigned', () => { + describe("|toLiteralDeepClone|", () => { + it("should verify will create literal deep cloned object from RequestPage object ", () => { + // Given + const requestPage = RequestPageImpl.of(9, 13); + // When - const instance = new RequestPageImpl(null, undefined); + const literal = requestPage.toLiteralCloneDeep(); // Then - expect(instance.page).toEqual(1); - expect(instance.size).toEqual(25); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RequestPageImpl.of(8, 20); - - // Then - expect(instance).toBeInstanceOf(RequestPageImpl); - expect(instance.page).toEqual(8); - expect(instance.size).toEqual(20); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with default values', () => { - // When - const instance = RequestPageImpl.empty(); - - // Then - expect(instance).toBeInstanceOf(RequestPageImpl); - expect(instance.page).toEqual(1); - expect(instance.size).toEqual(25); - }); - }); - - describe('|fromLiteral|', () => { - it('should verify will create new instance from given literal object', () => { - // Given - const literal = { pageNumber: 7, pageSize: 52 }; - - // When - const instance = RequestPageImpl.fromLiteral(literal); - - // Then - expect(instance).toBeInstanceOf(RequestPageImpl); - expect(instance.page).toEqual(7); - expect(instance.size).toEqual(52); - }); - }); - }); + expect(literal).not.toBeInstanceOf(RequestPageImpl); + expect(literal).toBeInstanceOf(Object); + expect(literal.pageNumber).toEqual(9); + expect(literal.pageSize).toEqual(13); + }); }); + }); +}); - describe('Methods::', () => { - describe('|toLiteral|', () => { - it('should verify will create literal object from RequestPage object ', () => { - // Given - const requestPage = RequestPageImpl.of(5, 17); - - // When - const literal = requestPage.toLiteral(); - - // Then - expect(literal).not.toBeInstanceOf(RequestPageImpl); - expect(literal).toBeInstanceOf(Object); - expect(literal.pageNumber).toEqual(5); - expect(literal.pageSize).toEqual(17); - }); +describe("RequestOrder", () => { + let apiPredicate1: ApiPredicate; + let apiPredicate2: ApiPredicate; + let apiPredicate3: ApiPredicate; + + beforeEach(() => { + apiPredicate1 = { pattern: "test*", property: "config.team", sort: "DESC" }; + apiPredicate2 = { pattern: "mock*", property: "config.job", sort: "ASC" }; + apiPredicate3 = { + pattern: "prod*", + property: "config.status", + sort: "DESC", + }; + }); + + it("should verify instance is created", () => { + // When + const instance = new RequestOrderImpl(); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(RequestOrderImpl); + }); + + it("should verify correct value are assigned", () => { + // When + const instance = new RequestOrderImpl( + apiPredicate1, + apiPredicate2, + apiPredicate3, + ); + + // Then + expect(instance.criteria).toBeInstanceOf(Array); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + it("should verify wont assign Nil parameters", () => { + // When + const instance = new RequestOrderImpl( + null, + apiPredicate2, + undefined, + apiPredicate1, + null, + apiPredicate3, + undefined, + ); + + // Then + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate2); + expect(instance.criteria[1]).toBe(apiPredicate1); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = RequestOrderImpl.of( + apiPredicate1, + null, + apiPredicate2, + apiPredicate3, + ); + + // Then + expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); }); + }); - describe('|toLiteralDeepClone|', () => { - it('should verify will create literal deep cloned object from RequestPage object ', () => { - // Given - const requestPage = RequestPageImpl.of(9, 13); + describe("|empty|", () => { + it("should verify will create empty instance with no criteria", () => { + // When + const instance = RequestOrderImpl.empty(); - // When - const literal = requestPage.toLiteralCloneDeep(); - - // Then - expect(literal).not.toBeInstanceOf(RequestPageImpl); - expect(literal).toBeInstanceOf(Object); - expect(literal.pageNumber).toEqual(9); - expect(literal.pageSize).toEqual(13); - }); + // Then + expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(instance.criteria).toEqual([]); + }); + }); + + describe("|fromLiteral|", () => { + it("should verify will create new instance from given literal Array of ApiPredicates", () => { + // Given + const literal: ApiPredicate[] = [ + apiPredicate1, + apiPredicate2, + apiPredicate3, + ]; + + // When + const instance = RequestOrderImpl.fromLiteral(literal); + + // Then + expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); }); + }); }); -}); + }); -describe('RequestOrder', () => { - let apiPredicate1: ApiPredicate; - let apiPredicate2: ApiPredicate; - let apiPredicate3: ApiPredicate; - - beforeEach(() => { - apiPredicate1 = { pattern: 'test*', property: 'config.team', sort: 'DESC' }; - apiPredicate2 = { pattern: 'mock*', property: 'config.job', sort: 'ASC' }; - apiPredicate3 = { - pattern: 'prod*', - property: 'config.status', - sort: 'DESC' - }; - }); + describe("Methods::", () => { + describe("|toLiteral|", () => { + it("should verify will create literal from RequestOrder object ", () => { + // Given + const requestOrder = RequestOrderImpl.of( + apiPredicate1, + null, + apiPredicate2, + undefined, + apiPredicate3, + ); - it('should verify instance is created', () => { // When - const instance = new RequestOrderImpl(); + const literal = requestOrder.toLiteral(); // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(literal).not.toBeInstanceOf(RequestOrderImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).toBe(apiPredicate1); + expect(literal[1]).toBe(apiPredicate2); + expect(literal[2]).toBe(apiPredicate3); + }); }); - it('should verify correct value are assigned', () => { - // When - const instance = new RequestOrderImpl(apiPredicate1, apiPredicate2, apiPredicate3); - - // Then - expect(instance.criteria).toBeInstanceOf(Array); - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate1); - expect(instance.criteria[1]).toBe(apiPredicate2); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); + describe("|toLiteralCloneDeep|", () => { + it("should verify will create literal deep cloned from RequestPage object ", () => { + // Given + const requestOrder = RequestOrderImpl.of( + apiPredicate3, + null, + apiPredicate1, + undefined, + apiPredicate2, + ); - it('should verify wont assign Nil parameters', () => { // When - const instance = new RequestOrderImpl(null, apiPredicate2, undefined, apiPredicate1, null, apiPredicate3, undefined); + const literal = requestOrder.toLiteralCloneDeep(); // Then - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate2); - expect(instance.criteria[1]).toBe(apiPredicate1); - expect(instance.criteria[2]).toBe(apiPredicate3); + expect(literal).not.toBeInstanceOf(RequestOrderImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).not.toBe(apiPredicate3); + expect(literal[1]).not.toBe(apiPredicate1); + expect(literal[2]).not.toBe(apiPredicate2); + expect(literal[0]).toEqual(apiPredicate3); + expect(literal[1]).toEqual(apiPredicate1); + expect(literal[2]).toEqual(apiPredicate2); + }); }); + }); +}); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RequestOrderImpl.of(apiPredicate1, null, apiPredicate2, apiPredicate3); - - // Then - expect(instance).toBeInstanceOf(RequestOrderImpl); - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate1); - expect(instance.criteria[1]).toBe(apiPredicate2); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with no criteria', () => { - // When - const instance = RequestOrderImpl.empty(); - - // Then - expect(instance).toBeInstanceOf(RequestOrderImpl); - expect(instance.criteria).toEqual([]); - }); - }); - - describe('|fromLiteral|', () => { - it('should verify will create new instance from given literal Array of ApiPredicates', () => { - // Given - const literal: ApiPredicate[] = [apiPredicate1, apiPredicate2, apiPredicate3]; - - // When - const instance = RequestOrderImpl.fromLiteral(literal); - - // Then - expect(instance).toBeInstanceOf(RequestOrderImpl); - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate1); - expect(instance.criteria[1]).toBe(apiPredicate2); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); - }); +describe("RequestFilter", () => { + let apiPredicate1: ApiPredicate; + let apiPredicate2: ApiPredicate; + let apiPredicate3: ApiPredicate; + + beforeEach(() => { + apiPredicate1 = { pattern: "test*", property: "config.team", sort: "DESC" }; + apiPredicate2 = { pattern: "mock*", property: "config.job", sort: "ASC" }; + apiPredicate3 = { + pattern: "prod*", + property: "config.status", + sort: "DESC", + }; + }); + + it("should verify instance is created", () => { + // When + const instance = new RequestFilterImpl(); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(RequestFilterImpl); + }); + + it("should verify correct value are assigned", () => { + // When + const instance = new RequestFilterImpl( + apiPredicate1, + apiPredicate2, + apiPredicate3, + ); + + // Then + expect(instance.criteria).toBeInstanceOf(Array); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + it("should verify wont assign Nil parameters", () => { + // When + const instance = new RequestFilterImpl( + null, + apiPredicate2, + undefined, + apiPredicate1, + null, + apiPredicate3, + undefined, + ); + + // Then + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate2); + expect(instance.criteria[1]).toBe(apiPredicate1); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = RequestFilterImpl.of( + apiPredicate1, + null, + apiPredicate2, + apiPredicate3, + ); + + // Then + expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); }); - }); + }); - describe('Methods::', () => { - describe('|toLiteral|', () => { - it('should verify will create literal from RequestOrder object ', () => { - // Given - const requestOrder = RequestOrderImpl.of(apiPredicate1, null, apiPredicate2, undefined, apiPredicate3); - - // When - const literal = requestOrder.toLiteral(); - - // Then - expect(literal).not.toBeInstanceOf(RequestOrderImpl); - expect(literal).toBeInstanceOf(Array); - expect(literal[0]).toBe(apiPredicate1); - expect(literal[1]).toBe(apiPredicate2); - expect(literal[2]).toBe(apiPredicate3); - }); - }); + describe("|empty|", () => { + it("should verify will create empty instance with no criteria", () => { + // When + const instance = RequestFilterImpl.empty(); - describe('|toLiteralCloneDeep|', () => { - it('should verify will create literal deep cloned from RequestPage object ', () => { - // Given - const requestOrder = RequestOrderImpl.of(apiPredicate3, null, apiPredicate1, undefined, apiPredicate2); - - // When - const literal = requestOrder.toLiteralCloneDeep(); - - // Then - expect(literal).not.toBeInstanceOf(RequestOrderImpl); - expect(literal).toBeInstanceOf(Array); - expect(literal[0]).not.toBe(apiPredicate3); - expect(literal[1]).not.toBe(apiPredicate1); - expect(literal[2]).not.toBe(apiPredicate2); - expect(literal[0]).toEqual(apiPredicate3); - expect(literal[1]).toEqual(apiPredicate1); - expect(literal[2]).toEqual(apiPredicate2); - }); + // Then + expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(instance.criteria).toEqual([]); + }); + }); + + describe("|fromLiteral|", () => { + it("should verify will create new instance from given literal Array of ApiPredicates", () => { + // Given + const literal: ApiPredicate[] = [ + apiPredicate1, + apiPredicate2, + apiPredicate3, + ]; + + // When + const instance = RequestFilterImpl.fromLiteral(literal); + + // Then + expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); }); + }); }); -}); + }); -describe('RequestFilter', () => { - let apiPredicate1: ApiPredicate; - let apiPredicate2: ApiPredicate; - let apiPredicate3: ApiPredicate; - - beforeEach(() => { - apiPredicate1 = { pattern: 'test*', property: 'config.team', sort: 'DESC' }; - apiPredicate2 = { pattern: 'mock*', property: 'config.job', sort: 'ASC' }; - apiPredicate3 = { - pattern: 'prod*', - property: 'config.status', - sort: 'DESC' - }; - }); + describe("Methods::", () => { + describe("|toLiteral|", () => { + it("should verify will create literal from RequestFilter object ", () => { + // Given + const requestFilter = RequestFilterImpl.of( + apiPredicate1, + null, + apiPredicate2, + undefined, + apiPredicate3, + ); - it('should verify instance is created', () => { // When - const instance = new RequestFilterImpl(); + const literal = requestFilter.toLiteral(); // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(literal).not.toBeInstanceOf(RequestFilterImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).toBe(apiPredicate1); + expect(literal[1]).toBe(apiPredicate2); + expect(literal[2]).toBe(apiPredicate3); + }); }); - it('should verify correct value are assigned', () => { - // When - const instance = new RequestFilterImpl(apiPredicate1, apiPredicate2, apiPredicate3); - - // Then - expect(instance.criteria).toBeInstanceOf(Array); - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate1); - expect(instance.criteria[1]).toBe(apiPredicate2); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); + describe("|toLiteralCloneDeep|", () => { + it("should verify will create literal deep cloned from RequestFilter object ", () => { + // Given + const requestFilter = RequestFilterImpl.of( + apiPredicate3, + null, + apiPredicate1, + undefined, + apiPredicate2, + ); - it('should verify wont assign Nil parameters', () => { // When - const instance = new RequestFilterImpl(null, apiPredicate2, undefined, apiPredicate1, null, apiPredicate3, undefined); + const literal = requestFilter.toLiteralCloneDeep(); // Then - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate2); - expect(instance.criteria[1]).toBe(apiPredicate1); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RequestFilterImpl.of(apiPredicate1, null, apiPredicate2, apiPredicate3); - - // Then - expect(instance).toBeInstanceOf(RequestFilterImpl); - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate1); - expect(instance.criteria[1]).toBe(apiPredicate2); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with no criteria', () => { - // When - const instance = RequestFilterImpl.empty(); - - // Then - expect(instance).toBeInstanceOf(RequestFilterImpl); - expect(instance.criteria).toEqual([]); - }); - }); - - describe('|fromLiteral|', () => { - it('should verify will create new instance from given literal Array of ApiPredicates', () => { - // Given - const literal: ApiPredicate[] = [apiPredicate1, apiPredicate2, apiPredicate3]; - - // When - const instance = RequestFilterImpl.fromLiteral(literal); - - // Then - expect(instance).toBeInstanceOf(RequestFilterImpl); - expect(instance.criteria.length).toEqual(3); - expect(instance.criteria[0]).toBe(apiPredicate1); - expect(instance.criteria[1]).toBe(apiPredicate2); - expect(instance.criteria[2]).toBe(apiPredicate3); - }); - }); - }); - }); - - describe('Methods::', () => { - describe('|toLiteral|', () => { - it('should verify will create literal from RequestFilter object ', () => { - // Given - const requestFilter = RequestFilterImpl.of(apiPredicate1, null, apiPredicate2, undefined, apiPredicate3); - - // When - const literal = requestFilter.toLiteral(); - - // Then - expect(literal).not.toBeInstanceOf(RequestFilterImpl); - expect(literal).toBeInstanceOf(Array); - expect(literal[0]).toBe(apiPredicate1); - expect(literal[1]).toBe(apiPredicate2); - expect(literal[2]).toBe(apiPredicate3); - }); - }); - - describe('|toLiteralCloneDeep|', () => { - it('should verify will create literal deep cloned from RequestFilter object ', () => { - // Given - const requestFilter = RequestFilterImpl.of(apiPredicate3, null, apiPredicate1, undefined, apiPredicate2); - - // When - const literal = requestFilter.toLiteralCloneDeep(); - - // Then - expect(literal).not.toBeInstanceOf(RequestFilterImpl); - expect(literal).toBeInstanceOf(Array); - expect(literal[0]).not.toBe(apiPredicate3); - expect(literal[1]).not.toBe(apiPredicate1); - expect(literal[2]).not.toBe(apiPredicate2); - expect(literal[0]).toEqual(apiPredicate3); - expect(literal[1]).toEqual(apiPredicate1); - expect(literal[2]).toEqual(apiPredicate2); - }); - }); + expect(literal).not.toBeInstanceOf(RequestFilterImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).not.toBe(apiPredicate3); + expect(literal[1]).not.toBe(apiPredicate1); + expect(literal[2]).not.toBe(apiPredicate2); + expect(literal[0]).toEqual(apiPredicate3); + expect(literal[1]).toEqual(apiPredicate1); + expect(literal[2]).toEqual(apiPredicate2); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts index 42ad7afffc..27639063d2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts @@ -3,185 +3,188 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Literal } from '../../interfaces'; -import { CollectionsUtil } from '../../../utils'; +import { Literal } from "../../interfaces"; +import { CollectionsUtil } from "../../../utils"; // Page DTO export type LiteralRequestPage = { pageNumber: number; pageSize: number }; export interface RequestPage extends Literal { - readonly page: number; - readonly size: number; + readonly page: number; + readonly size: number; } /** * ** Request Page DTO. */ export class RequestPageImpl implements RequestPage { - public readonly page: number; - public readonly size: number; - - constructor(page: number, size: number) { - this.page = page ?? 1; - this.size = size ?? 25; - } - - /** - * ** Factory method. - */ - static of(page: number, size: number): RequestPageImpl { - return new RequestPageImpl(page, size); - } - - /** - * ** Factory method for empty RequestPageDTO. - */ - static empty(): RequestPageImpl { - return new RequestPageImpl(null, null); - } - - /** - * ** Creates DTO from literal. - */ - static fromLiteral(literalDTO: { pageNumber: number; pageSize: number }): RequestPageImpl { - return RequestPageImpl.of(literalDTO.pageNumber, literalDTO.pageSize); - } - - /** - * @inheritDoc - */ - toLiteral(): LiteralRequestPage { - return { - pageNumber: this.page ?? 1, - pageSize: this.size ?? 25 - }; - } - - /** - * @inheritDoc - */ - toLiteralCloneDeep(): LiteralRequestPage { - return this.toLiteral(); - } + public readonly page: number; + public readonly size: number; + + constructor(page: number, size: number) { + this.page = page ?? 1; + this.size = size ?? 25; + } + + /** + * ** Factory method. + */ + static of(page: number, size: number): RequestPageImpl { + return new RequestPageImpl(page, size); + } + + /** + * ** Factory method for empty RequestPageDTO. + */ + static empty(): RequestPageImpl { + return new RequestPageImpl(null, null); + } + + /** + * ** Creates DTO from literal. + */ + static fromLiteral(literalDTO: { + pageNumber: number; + pageSize: number; + }): RequestPageImpl { + return RequestPageImpl.of(literalDTO.pageNumber, literalDTO.pageSize); + } + + /** + * @inheritDoc + */ + toLiteral(): LiteralRequestPage { + return { + pageNumber: this.page ?? 1, + pageSize: this.size ?? 25, + }; + } + + /** + * @inheritDoc + */ + toLiteralCloneDeep(): LiteralRequestPage { + return this.toLiteral(); + } } // Order DTO export interface RequestOrder extends Literal { - readonly criteria: ApiPredicate[]; + readonly criteria: ApiPredicate[]; } /** * ** Request Order DTO. */ export class RequestOrderImpl implements RequestOrder { - public readonly criteria: ApiPredicate[]; - - constructor(...criteria: ApiPredicate[]) { - // eslint-disable-next-line @typescript-eslint/unbound-method - this.criteria = [...criteria.filter(CollectionsUtil.isDefined)]; - } - - /** - * ** Factory method. - */ - static of(...criteria: ApiPredicate[]): RequestOrderImpl { - return new RequestOrderImpl(...criteria); - } - - /** - * ** Factory method for empty RequestOrderDTO. - */ - static empty(): RequestOrderImpl { - return new RequestOrderImpl(); - } - - /** - * ** Creates DTO from literal. - */ - static fromLiteral(literalCriteria: Array): RequestOrderImpl { - return RequestOrderImpl.of(...literalCriteria); - } - - /** - * @inheritDoc - */ - toLiteral(): LiteralApiPredicates { - return [...this.criteria]; - } - - /** - * @inheritDoc - */ - toLiteralCloneDeep(): LiteralApiPredicates { - return this.criteria.map((c) => ({ ...c })); - } + public readonly criteria: ApiPredicate[]; + + constructor(...criteria: ApiPredicate[]) { + // eslint-disable-next-line @typescript-eslint/unbound-method + this.criteria = [...criteria.filter(CollectionsUtil.isDefined)]; + } + + /** + * ** Factory method. + */ + static of(...criteria: ApiPredicate[]): RequestOrderImpl { + return new RequestOrderImpl(...criteria); + } + + /** + * ** Factory method for empty RequestOrderDTO. + */ + static empty(): RequestOrderImpl { + return new RequestOrderImpl(); + } + + /** + * ** Creates DTO from literal. + */ + static fromLiteral(literalCriteria: Array): RequestOrderImpl { + return RequestOrderImpl.of(...literalCriteria); + } + + /** + * @inheritDoc + */ + toLiteral(): LiteralApiPredicates { + return [...this.criteria]; + } + + /** + * @inheritDoc + */ + toLiteralCloneDeep(): LiteralApiPredicates { + return this.criteria.map((c) => ({ ...c })); + } } // Filter DTO export interface RequestFilter extends Literal { - readonly criteria: ApiPredicate[]; + readonly criteria: ApiPredicate[]; } /** * ** Request Filter DTO. */ export class RequestFilterImpl implements RequestFilter { - public readonly criteria: ApiPredicate[]; - - constructor(...criteria: ApiPredicate[]) { - // eslint-disable-next-line @typescript-eslint/unbound-method - this.criteria = [...criteria.filter(CollectionsUtil.isDefined)]; - } - - /** - * ** Factory method. - */ - static of(...criteria: ApiPredicate[]): RequestFilterImpl { - return new RequestFilterImpl(...criteria); - } - - /** - * ** Factory method for empty RequestFilterDTO. - */ - static empty(): RequestFilterImpl { - return new RequestFilterImpl(); - } - - /** - * ** Creates DTO from literal. - */ - static fromLiteral(literalCriteria: Array): RequestFilterImpl { - return RequestFilterImpl.of(...literalCriteria); - } - - /** - * @inheritDoc - */ - toLiteral(): LiteralApiPredicates { - return [...this.criteria]; - } - - /** - * @inheritDoc - */ - toLiteralCloneDeep(): LiteralApiPredicates { - return this.criteria.map((c) => ({ ...c })); - } + public readonly criteria: ApiPredicate[]; + + constructor(...criteria: ApiPredicate[]) { + // eslint-disable-next-line @typescript-eslint/unbound-method + this.criteria = [...criteria.filter(CollectionsUtil.isDefined)]; + } + + /** + * ** Factory method. + */ + static of(...criteria: ApiPredicate[]): RequestFilterImpl { + return new RequestFilterImpl(...criteria); + } + + /** + * ** Factory method for empty RequestFilterDTO. + */ + static empty(): RequestFilterImpl { + return new RequestFilterImpl(); + } + + /** + * ** Creates DTO from literal. + */ + static fromLiteral(literalCriteria: Array): RequestFilterImpl { + return RequestFilterImpl.of(...literalCriteria); + } + + /** + * @inheritDoc + */ + toLiteral(): LiteralApiPredicates { + return [...this.criteria]; + } + + /** + * @inheritDoc + */ + toLiteralCloneDeep(): LiteralApiPredicates { + return this.criteria.map((c) => ({ ...c })); + } } // Generic Predicate for API export type LiteralApiPredicates = Array; -export const ASC = 'ASC'; -export const DESC = 'DESC'; +export const ASC = "ASC"; +export const DESC = "DESC"; export type DirectionType = typeof ASC | typeof DESC; export interface ApiPredicate { - readonly property: string; - readonly pattern: string; - readonly sort: DirectionType; + readonly property: string; + readonly pattern: string; + readonly sort: DirectionType; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts index f5459178f6..f321780abb 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './criteria'; -export * from './error'; -export * from './http'; -export * from './interfaces'; -export * from './object'; -export * from './predicate'; -export * from './route'; -export * from './service'; -export * from './tasks'; +export * from "./criteria"; +export * from "./error"; +export * from "./http"; +export * from "./interfaces"; +export * from "./object"; +export * from "./predicate"; +export * from "./route"; +export * from "./service"; +export * from "./tasks"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts index 47b610758a..bb93f3907e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts @@ -7,62 +7,62 @@ * ** Interface for Comparison data. */ export interface Comparable { - /** - * ** Value stored in Comparable for Comparison. - */ - readonly value: T; + /** + * ** Value stored in Comparable for Comparison. + */ + readonly value: T; - /** - * ** Compares stored data with the provided one. - * - * -1 if the stored value is less than the provided value. - * 0 if both values are equal. - * 1 if stored value is bigger than the provided value. - */ - compare(comparable: Comparable): number; + /** + * ** Compares stored data with the provided one. + * + * -1 if the stored value is less than the provided value. + * 0 if both values are equal. + * 1 if stored value is bigger than the provided value. + */ + compare(comparable: Comparable): number; - /** - * ** Verify if stored value is null or undefined and returns true. - */ - isNil(): boolean; + /** + * ** Verify if stored value is null or undefined and returns true. + */ + isNil(): boolean; - /** - * ** Verify if stored value is not null and not undefined. - */ - notNil(): boolean; + /** + * ** Verify if stored value is not null and not undefined. + */ + notNil(): boolean; - /** - * ** Compare if stored value is similar to provided value. - */ - like(comparable: Comparable): boolean; + /** + * ** Compare if stored value is similar to provided value. + */ + like(comparable: Comparable): boolean; - /** - * ** Verify if stored value is equals with provided value. - */ - equal(comparable: Comparable): boolean; + /** + * ** Verify if stored value is equals with provided value. + */ + equal(comparable: Comparable): boolean; - /** - * ** Verify if stored value is different than provided value. - */ - notEqual(comparable: Comparable): boolean; + /** + * ** Verify if stored value is different than provided value. + */ + notEqual(comparable: Comparable): boolean; - /** - * ** Verify if stored value is less than provided value. - */ - lessThan(comparable: Comparable): boolean; + /** + * ** Verify if stored value is less than provided value. + */ + lessThan(comparable: Comparable): boolean; - /** - * ** Verify if stored value is less or equal than provided value. - */ - lessThanInclusive(comparable: Comparable): boolean; + /** + * ** Verify if stored value is less or equal than provided value. + */ + lessThanInclusive(comparable: Comparable): boolean; - /** - * ** Verify if stored value is greater than provided value. - */ - greaterThan(comparable: Comparable): boolean; + /** + * ** Verify if stored value is greater than provided value. + */ + greaterThan(comparable: Comparable): boolean; - /** - * ** Verify if stored value is greater or equal than provided value. - */ - greaterThanInclusive(comparable: Comparable): boolean; + /** + * ** Verify if stored value is greater or equal than provided value. + */ + greaterThanInclusive(comparable: Comparable): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparator.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparator.interface.ts index b677fe9c7c..0cb11b0ae3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparator.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparator.interface.ts @@ -7,8 +7,8 @@ * ** Comparator interface. */ export interface Comparator { - /** - * ** Executes comparison between two values. - */ - compare(a: T, b: T): -1 | 0 | 1 | number; + /** + * ** Executes comparison between two values. + */ + compare(a: T, b: T): -1 | 0 | 1 | number; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts index b442ae0323..bc79f6ef14 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts @@ -9,9 +9,9 @@ * ** Interface for Copy of Object. */ export interface Copy> { - /** - * ** Make shallow copy of current Object. - * - Optionally provide partial Object to merge on top of the current one. - */ - copy(partial?: Partial); + /** + * ** Make shallow copy of current Object. + * - Optionally provide partial Object to merge on top of the current one. + */ + copy(partial?: Partial); } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/criteria.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/criteria.interface.ts index b05b638d3b..be5cc8736a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/criteria.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/criteria.interface.ts @@ -7,10 +7,10 @@ * ** Interface for filtering data according some criteria. */ export interface Criteria { - /** - * ** Creates new filtered Array of elements that meets the criteria. - * - * - Does not modify the original Array. - */ - meetCriteria(elements: T[]): T[]; + /** + * ** Creates new filtered Array of elements that meets the criteria. + * + * - Does not modify the original Array. + */ + meetCriteria(elements: T[]): T[]; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/equals.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/equals.interface.ts index 27f5222b7f..5521db89f9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/equals.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/equals.interface.ts @@ -9,8 +9,8 @@ * ** Interface for Equality of two Object. */ export interface Equals> { - /** - * ** Make equality comparison between two objects of same type, current and provided. - */ - equals(obj: T): boolean; + /** + * ** Make equality comparison between two objects of same type, current and provided. + */ + equals(obj: T): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts index e4d68d2a76..3d87974892 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts @@ -3,20 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable } from './comparable.interface'; -import { Predicate } from './predicate.interface'; +import { Comparable } from "./comparable.interface"; +import { Predicate } from "./predicate.interface"; /** * ** Interface for Expression. */ export interface Expression { - /** - * ** Predicates Array. - */ - readonly predicates: T[]; + /** + * ** Predicates Array. + */ + readonly predicates: T[]; - /** - * ** Evaluate Expression to boolean (true or false). - */ - evaluate(comparable?: Comparable): boolean; + /** + * ** Evaluate Expression to boolean (true or false). + */ + evaluate(comparable?: Comparable): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts index f092cd31d0..398bed6b28 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './comparable.interface'; -export * from './comparator.interface'; -export * from './copy.interface'; -export * from './criteria.interface'; -export * from './equals.interface'; -export * from './expression.interface'; -export * from './literal.interface'; -export * from './predicate.interface'; -export * from './replacer.interface'; -export * from './serializable.interface'; +export * from "./comparable.interface"; +export * from "./comparator.interface"; +export * from "./copy.interface"; +export * from "./criteria.interface"; +export * from "./equals.interface"; +export * from "./expression.interface"; +export * from "./literal.interface"; +export * from "./predicate.interface"; +export * from "./replacer.interface"; +export * from "./serializable.interface"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts index 9db6cd3c47..d1bb3aeed0 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts @@ -3,24 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PrimitivesNil, PrimitivesNilArrays, PrimitivesNilObject } from '../../utils'; +import { + PrimitivesNil, + PrimitivesNilArrays, + PrimitivesNilObject, +} from "../../utils"; -type SerializedType = PrimitivesNil | PrimitivesNilArrays | PrimitivesNilObject | unknown; +type SerializedType = + | PrimitivesNil + | PrimitivesNilArrays + | PrimitivesNilObject + | unknown; /** * ** This interface gives boundaries for Class instances to get converted into Literals. */ export interface Literal { - /** - * ** Implements this method and return data you want to be serialized into Literals. - */ - toLiteral(): T; + /** + * ** Implements this method and return data you want to be serialized into Literals. + */ + toLiteral(): T; - /** - * ** Implements this method and return data you want to be serialized into Literals. - *

    - * - Data should be deep clone before return, to comply with immutability. - *

    - */ - toLiteralCloneDeep(): T; + /** + * ** Implements this method and return data you want to be serialized into Literals. + *

    + * - Data should be deep clone before return, to comply with immutability. + *

    + */ + toLiteralCloneDeep(): T; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts index 5f682a3561..e5e755b02a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts @@ -3,19 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable } from './comparable.interface'; +import { Comparable } from "./comparable.interface"; /** * ** Interface for Predicate Classes. */ -export interface Predicate { - /** - * ** Stored comparable that have to be compared with provided comparable. - */ - readonly comparable: T; +export interface Predicate< + T extends Comparable = Comparable, + C extends Comparable = T, +> { + /** + * ** Stored comparable that have to be compared with provided comparable. + */ + readonly comparable: T; - /** - * ** Evaluate Predicate to boolean (true or false). - */ - evaluate(comparable: C): boolean; + /** + * ** Evaluate Predicate to boolean (true or false). + */ + evaluate(comparable: C): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts index 0bc8084eed..37746520cd 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts @@ -7,6 +7,6 @@ * ** Interface for generic replacer. */ export interface Replacer { - searchValue: T; - replaceValue: T; + searchValue: T; + replaceValue: T; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts index ba27569ab5..fbe790ae14 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts @@ -3,16 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PrimitivesNil, PrimitivesNilArrays, PrimitivesNilObject } from '../../utils'; +import { + PrimitivesNil, + PrimitivesNilArrays, + PrimitivesNilObject, +} from "../../utils"; -type SerializedType = PrimitivesNil | PrimitivesNilArrays | PrimitivesNilObject | unknown; +type SerializedType = + | PrimitivesNil + | PrimitivesNilArrays + | PrimitivesNilObject + | unknown; /** * ** This interface gives boundaries for Objects that we want to be serializable for JSON. */ export interface Serializable { - /** - * ** Implements this method and return data you want to be serialized when JSON.stringify(...) is executed. - */ - toJSON(): T; + /** + * ** Implements this method and return data you want to be serialized when JSON.stringify(...) is executed. + */ + toJSON(): T; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts index 6b7b2bd780..c32b85c4bc 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './model'; +export * from "./model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts index 572589f9c4..675f138228 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './taurus-object.model'; +export * from "./taurus-object.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts index 32201b1265..d62b3f27e5 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts @@ -5,152 +5,185 @@ /* eslint-disable @typescript-eslint/dot-notation */ -import { Subscription } from 'rxjs'; +import { Subscription } from "rxjs"; -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { TaurusObject } from './taurus-object.model'; +import { TaurusObject } from "./taurus-object.model"; -describe('TaurusObject', () => { - it('should verify instance is created', () => { - // When +describe("TaurusObject", () => { + it("should verify instance is created", () => { + // When + const instance = new TaurusObject(); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(TaurusObject); + }); + + describe("Properties::", () => { + describe("|subscriptions|", () => { + it("should verify default value is empty Array", () => { + // Given + const instance = new TaurusObject(); + + // Then + expect(instance["subscriptions"]).toEqual([]); + }); + }); + }); + + describe("Methods::", () => { + describe("|dispose|", () => { + it("should verify will clean all subscriptions", () => { + // Given + const subscription1 = jasmine.createSpyObj( + "subscription1", + ["unsubscribe"], + ); + const subscription2 = jasmine.createSpyObj( + "subscription2", + ["unsubscribe"], + ); + const subscription3 = jasmine.createSpyObj( + "subscription3", + ["unsubscribe"], + ); const instance = new TaurusObject(); + instance["subscriptions"].push( + subscription1, + null, + subscription2, + undefined, + subscription3, + ); + + // When + instance.dispose(); // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(TaurusObject); + expect(subscription1.unsubscribe).toHaveBeenCalled(); + expect(subscription2.unsubscribe).toHaveBeenCalled(); + expect(subscription3.unsubscribe).toHaveBeenCalled(); + }); }); - describe('Properties::', () => { - describe('|subscriptions|', () => { - it('should verify default value is empty Array', () => { - // Given - const instance = new TaurusObject(); + describe("|removeSubscriptionRef|", () => { + let subscription1: Subscription; + let subscription2: Subscription; + let subscription3: Subscription; + let instance: TaurusObject; + + beforeEach(() => { + subscription1 = new Subscription(); + subscription2 = new Subscription(); + subscription3 = new Subscription(); + instance = new TaurusObject(); + }); + + it("should verify will remove provided subscription from buffer", () => { + // Given + instance["subscriptions"].push( + subscription1, + null, + undefined, + subscription2, + subscription3, + ); + const unsubscribeSpy = spyOn( + subscription2, + "unsubscribe", + ).and.callThrough(); + + // Then 1 + expect(instance["subscriptions"].length).toEqual(5); + + // When + // @ts-ignore + const value = instance.removeSubscriptionRef(subscription2); + + // Then 2 + expect(value).toBeTrue(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(instance["subscriptions"].length).toEqual(4); + }); + + it("should verify will remove provided subscription from buffer and unsubscribe error will be logged in console", () => { + // Given + instance["subscriptions"].push(subscription1); + const error = new Error("Error"); + const unsubscribeSpy = spyOn( + subscription1, + "unsubscribe", + ).and.throwError(error); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + // When + // @ts-ignore + const value = instance.removeSubscriptionRef(subscription1); + + // Then 2 + expect(value).toBeFalse(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Taurus Object failed to unsubscribe from rxjs stream!`, + error, + ); + }); + + it(`should verify will unsubscribe even reference doesn't exist in buffer`, () => { + // Given + instance["subscriptions"].push(subscription2, null, undefined); + const unsubscribeSpy = spyOn( + subscription1, + "unsubscribe", + ).and.callThrough(); + + // Then 1 + expect(instance["subscriptions"].length).toEqual(3); + + // When + // @ts-ignore + const value = instance.removeSubscriptionRef(subscription1); + + // Then 2 + expect(value).toBeTrue(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(instance["subscriptions"].length).toEqual(3); + }); + + it(`should verify will return false when provided reference is not Subscription instance`, () => { + // Given + instance["subscriptions"].push(subscription1, subscription2); + + // Then 1 + expect(instance["subscriptions"].length).toEqual(2); - // Then - expect(instance['subscriptions']).toEqual([]); - }); - }); + // When + // @ts-ignore + const value1 = instance.removeSubscriptionRef(null); + // @ts-ignore + const value2 = instance.removeSubscriptionRef(undefined); + + // Then 2 + expect(value1).toBeFalse(); + expect(value2).toBeFalse(); + expect(instance["subscriptions"].length).toEqual(2); + }); }); - describe('Methods::', () => { - describe('|dispose|', () => { - it('should verify will clean all subscriptions', () => { - // Given - const subscription1 = jasmine.createSpyObj('subscription1', ['unsubscribe']); - const subscription2 = jasmine.createSpyObj('subscription2', ['unsubscribe']); - const subscription3 = jasmine.createSpyObj('subscription3', ['unsubscribe']); - const instance = new TaurusObject(); - instance['subscriptions'].push(subscription1, null, subscription2, undefined, subscription3); - - // When - instance.dispose(); - - // Then - expect(subscription1.unsubscribe).toHaveBeenCalled(); - expect(subscription2.unsubscribe).toHaveBeenCalled(); - expect(subscription3.unsubscribe).toHaveBeenCalled(); - }); - }); - - describe('|removeSubscriptionRef|', () => { - let subscription1: Subscription; - let subscription2: Subscription; - let subscription3: Subscription; - let instance: TaurusObject; - - beforeEach(() => { - subscription1 = new Subscription(); - subscription2 = new Subscription(); - subscription3 = new Subscription(); - instance = new TaurusObject(); - }); - - it('should verify will remove provided subscription from buffer', () => { - // Given - instance['subscriptions'].push(subscription1, null, undefined, subscription2, subscription3); - const unsubscribeSpy = spyOn(subscription2, 'unsubscribe').and.callThrough(); - - // Then 1 - expect(instance['subscriptions'].length).toEqual(5); - - // When - // @ts-ignore - const value = instance.removeSubscriptionRef(subscription2); - - // Then 2 - expect(value).toBeTrue(); - expect(unsubscribeSpy).toHaveBeenCalled(); - expect(instance['subscriptions'].length).toEqual(4); - }); - - it('should verify will remove provided subscription from buffer and unsubscribe error will be logged in console', () => { - // Given - instance['subscriptions'].push(subscription1); - const error = new Error('Error'); - const unsubscribeSpy = spyOn(subscription1, 'unsubscribe').and.throwError(error); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // When - // @ts-ignore - const value = instance.removeSubscriptionRef(subscription1); - - // Then 2 - expect(value).toBeFalse(); - expect(unsubscribeSpy).toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith(`Taurus Object failed to unsubscribe from rxjs stream!`, error); - }); - - it(`should verify will unsubscribe even reference doesn't exist in buffer`, () => { - // Given - instance['subscriptions'].push(subscription2, null, undefined); - const unsubscribeSpy = spyOn(subscription1, 'unsubscribe').and.callThrough(); - - // Then 1 - expect(instance['subscriptions'].length).toEqual(3); - - // When - // @ts-ignore - const value = instance.removeSubscriptionRef(subscription1); - - // Then 2 - expect(value).toBeTrue(); - expect(unsubscribeSpy).toHaveBeenCalled(); - expect(instance['subscriptions'].length).toEqual(3); - }); - - it(`should verify will return false when provided reference is not Subscription instance`, () => { - // Given - instance['subscriptions'].push(subscription1, subscription2); - - // Then 1 - expect(instance['subscriptions'].length).toEqual(2); - - // When - // @ts-ignore - const value1 = instance.removeSubscriptionRef(null); - // @ts-ignore - const value2 = instance.removeSubscriptionRef(undefined); - - // Then 2 - expect(value1).toBeFalse(); - expect(value2).toBeFalse(); - expect(instance['subscriptions'].length).toEqual(2); - }); - }); - - describe('|ngOnDestroy|', () => { - it('should verify will invoke correct method', () => { - // Given - const instance = new TaurusObject(); - const disposeSpy = spyOn(instance, 'dispose').and.callFake(CallFake); - - // When - instance.ngOnDestroy(); - - // Then - expect(disposeSpy).toHaveBeenCalled(); - }); - }); + describe("|ngOnDestroy|", () => { + it("should verify will invoke correct method", () => { + // Given + const instance = new TaurusObject(); + const disposeSpy = spyOn(instance, "dispose").and.callFake(CallFake); + + // When + instance.ngOnDestroy(); + + // Then + expect(disposeSpy).toHaveBeenCalled(); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts index ec5f3e3137..a940303c94 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive, OnDestroy } from '@angular/core'; +import { Directive, OnDestroy } from "@angular/core"; -import { Subscription } from 'rxjs'; +import { Subscription } from "rxjs"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; /** * ** Base Class for all Angular related Objects. @@ -17,108 +17,112 @@ import { CollectionsUtil } from '../../../utils'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix export class TaurusObject implements OnDestroy { - /** - * ** Class name, identifier for Object class. - * - * - Format should be PascalCase. - */ - static readonly CLASS_NAME: string = 'TaurusObject'; - - /** - * ** Class PUBLIC_NAME, human-readable. - * - * - Format should be Kebab-Case. - */ - static readonly PUBLIC_NAME: string = 'Taurus-Base-Object'; - - /** - * ** Object UUID that meats RFC4122 compliance and also has Class name identifier inside. - * - *
    - * pattern: - *

    - * _ - *

    - */ - readonly objectUUID: string; - - /** - * ** Store for Subscriptions references. - */ - protected subscriptions: Subscription[]; - - /** - * ** Constructor. - */ - constructor(className: string = null) { - this.objectUUID = CollectionsUtil.generateObjectUUID(className ?? TaurusObject.CLASS_NAME); - this.subscriptions = []; + /** + * ** Class name, identifier for Object class. + * + * - Format should be PascalCase. + */ + static readonly CLASS_NAME: string = "TaurusObject"; + + /** + * ** Class PUBLIC_NAME, human-readable. + * + * - Format should be Kebab-Case. + */ + static readonly PUBLIC_NAME: string = "Taurus-Base-Object"; + + /** + * ** Object UUID that meats RFC4122 compliance and also has Class name identifier inside. + * + *
    + * pattern: + *

    + * _ + *

    + */ + readonly objectUUID: string; + + /** + * ** Store for Subscriptions references. + */ + protected subscriptions: Subscription[]; + + /** + * ** Constructor. + */ + constructor(className: string = null) { + this.objectUUID = CollectionsUtil.generateObjectUUID( + className ?? TaurusObject.CLASS_NAME, + ); + this.subscriptions = []; + } + + /** + * ** Methods that will dispose Object. + * - Clean all Subscriptions. + */ + dispose(): void { + this.cleanSubscriptions(); + } + + /** + * @inheritDoc + */ + ngOnDestroy() { + this.dispose(); + } + + /** + * ** Clean all Subscriptions. + */ + protected cleanSubscriptions(): void { + // unsubscribe all valid subscriptions + this.subscriptions + // eslint-disable-next-line @typescript-eslint/unbound-method + .filter(CollectionsUtil.isDefined) + // eslint-disable-next-line @typescript-eslint/unbound-method + .forEach(TaurusObject._unsubscribeFromStream); + } + + /** + * ** Remove subscription reference from subscriptions queue providing reference itself. + * + * - Before remove it would be auto-unsubscribed from stream. + * @protected + */ + protected removeSubscriptionRef(subscriptionRef: Subscription): boolean { + const subscriptionIndex = this.subscriptions.findIndex( + (s) => s === subscriptionRef, + ); + + if (subscriptionIndex === -1) { + if (subscriptionRef instanceof Subscription) { + TaurusObject._unsubscribeFromStream(subscriptionRef); + + return true; + } + + return false; } - /** - * ** Methods that will dispose Object. - * - Clean all Subscriptions. - */ - dispose(): void { - this.cleanSubscriptions(); - } - - /** - * @inheritDoc - */ - ngOnDestroy() { - this.dispose(); - } - - /** - * ** Clean all Subscriptions. - */ - protected cleanSubscriptions(): void { - // unsubscribe all valid subscriptions - this.subscriptions - // eslint-disable-next-line @typescript-eslint/unbound-method - .filter(CollectionsUtil.isDefined) - // eslint-disable-next-line @typescript-eslint/unbound-method - .forEach(TaurusObject._unsubscribeFromStream); - } - - /** - * ** Remove subscription reference from subscriptions queue providing reference itself. - * - * - Before remove it would be auto-unsubscribed from stream. - * @protected - */ - protected removeSubscriptionRef(subscriptionRef: Subscription): boolean { - const subscriptionIndex = this.subscriptions.findIndex((s) => s === subscriptionRef); + const removedSubscription = this.subscriptions.splice(subscriptionIndex, 1); - if (subscriptionIndex === -1) { - if (subscriptionRef instanceof Subscription) { - TaurusObject._unsubscribeFromStream(subscriptionRef); - - return true; - } - - return false; - } - - const removedSubscription = this.subscriptions.splice(subscriptionIndex, 1); - - return TaurusObject._unsubscribeFromStream(removedSubscription[0]); - } + return TaurusObject._unsubscribeFromStream(removedSubscription[0]); + } - /** - * ** Unsubscribe subscription from stream. - * @private - */ - private static _unsubscribeFromStream(s: Subscription): boolean { - try { - s.unsubscribe(); + /** + * ** Unsubscribe subscription from stream. + * @private + */ + private static _unsubscribeFromStream(s: Subscription): boolean { + try { + s.unsubscribe(); - return true; - } catch (e) { - console.error(`Taurus Object failed to unsubscribe from rxjs stream!`, e); + return true; + } catch (e) { + console.error(`Taurus Object failed to unsubscribe from rxjs stream!`, e); - return false; - } + return false; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts index 36226ef5ef..03646c294f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts @@ -3,301 +3,301 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable } from '../../interfaces'; +import { Comparable } from "../../interfaces"; -import { ComparableImpl } from './comparable.impl'; +import { ComparableImpl } from "./comparable.impl"; class ComparableStub implements Comparable { - public readonly value: unknown; + public readonly value: unknown; - constructor(value: unknown) { - this.value = value; - } + constructor(value: unknown) { + this.value = value; + } - compare(_comparable: Comparable): number { - return 0; - } + compare(_comparable: Comparable): number { + return 0; + } - equal(_comparable: Comparable): boolean { - return true; - } + equal(_comparable: Comparable): boolean { + return true; + } - like(_comparable: Comparable): boolean { - return true; - } + like(_comparable: Comparable): boolean { + return true; + } - notEqual(_comparable: Comparable): boolean { - return false; - } + notEqual(_comparable: Comparable): boolean { + return false; + } - isNil(): boolean { - return false; - } + isNil(): boolean { + return false; + } - notNil(): boolean { - return true; - } + notNil(): boolean { + return true; + } - greaterThan(_comparable: Comparable): boolean { - return false; - } + greaterThan(_comparable: Comparable): boolean { + return false; + } - greaterThanInclusive(_comparable: Comparable): boolean { - return false; - } + greaterThanInclusive(_comparable: Comparable): boolean { + return false; + } - lessThan(_comparable: Comparable): boolean { - return false; - } + lessThan(_comparable: Comparable): boolean { + return false; + } - lessThanInclusive(_comparable: Comparable): boolean { - return false; - } + lessThanInclusive(_comparable: Comparable): boolean { + return false; + } } -describe('ComparableImpl', () => { - it('should verify instance is created', () => { - // Given - const value = 'VDK'; +describe("ComparableImpl", () => { + it("should verify instance is created", () => { + // Given + const value = "VDK"; - // When - const instance = new ComparableImpl(value); + // When + const instance = new ComparableImpl(value); - // Then - expect(instance).toBeDefined(); + // Then + expect(instance).toBeDefined(); + }); + + it("should verify value is correctly assigned", () => { + // Given + const value = "VDK"; + + // When + const instance = new ComparableImpl(value); + + // Then + expect(instance.value).toBe(value); + }); + + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // Given + const value = "VDK"; + + // When + const instance = ComparableImpl.of(value); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(ComparableImpl); + }); + }); + }); + }); + + describe("Methods::()", () => { + let v1: unknown; + let v2: unknown; + let v3: unknown; + let v4: unknown; + let v5: unknown; + let v6: unknown; + let v7: unknown; + let v8: unknown; + + let c1: ComparableImpl; + let c2: ComparableImpl; + let c3: ComparableImpl; + let c4: ComparableImpl; + let c5: ComparableImpl; + let c6: ComparableImpl; + let c7: ComparableImpl; + let c8: ComparableImpl; + + beforeEach(() => { + v1 = null; + v2 = undefined; + v3 = "Saggitarius"; + v4 = 10; + v5 = "Saggitarius"; + v6 = 11; + v7 = 10; + v8 = "Taurus"; + + c1 = ComparableImpl.of(v1); + c2 = ComparableImpl.of(v2); + c3 = ComparableImpl.of(v3); + c4 = ComparableImpl.of(v4); + c5 = ComparableImpl.of(v5); + c6 = ComparableImpl.of(v6); + c7 = ComparableImpl.of(v7); + c8 = ComparableImpl.of(v8); }); - it('should verify value is correctly assigned', () => { - // Given - const value = 'VDK'; + describe("|compare|", () => { + it("should verify will return 0 for equal", () => { + // When + const comparison = c3.compare(c5); + + // Then + expect(comparison).toEqual(0); + }); + it("should verify will return 1 for greaterThan", () => { // When - const instance = new ComparableImpl(value); + const comparison = c8.compare(c5); // Then - expect(instance.value).toBe(value); - }); + expect(comparison).toEqual(1); + }); - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // Given - const value = 'VDK'; - - // When - const instance = ComparableImpl.of(value); - - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(ComparableImpl); - }); - }); - }); - }); + it("should verify will return -1 for lessThan", () => { + // When + const comparison = c5.compare(c8); - describe('Methods::()', () => { - let v1: unknown; - let v2: unknown; - let v3: unknown; - let v4: unknown; - let v5: unknown; - let v6: unknown; - let v7: unknown; - let v8: unknown; - - let c1: ComparableImpl; - let c2: ComparableImpl; - let c3: ComparableImpl; - let c4: ComparableImpl; - let c5: ComparableImpl; - let c6: ComparableImpl; - let c7: ComparableImpl; - let c8: ComparableImpl; - - beforeEach(() => { - v1 = null; - v2 = undefined; - v3 = 'Saggitarius'; - v4 = 10; - v5 = 'Saggitarius'; - v6 = 11; - v7 = 10; - v8 = 'Taurus'; - - c1 = ComparableImpl.of(v1); - c2 = ComparableImpl.of(v2); - c3 = ComparableImpl.of(v3); - c4 = ComparableImpl.of(v4); - c5 = ComparableImpl.of(v5); - c6 = ComparableImpl.of(v6); - c7 = ComparableImpl.of(v7); - c8 = ComparableImpl.of(v8); - }); + // Then + expect(comparison).toEqual(-1); + }); - describe('|compare|', () => { - it('should verify will return 0 for equal', () => { - // When - const comparison = c3.compare(c5); + it("should verify will return -1 given comparable is not instance of the current Constructor", () => { + // Given + const c10 = new ComparableStub(v2); - // Then - expect(comparison).toEqual(0); - }); + // When + const comparison = c5.compare(c10); - it('should verify will return 1 for greaterThan', () => { - // When - const comparison = c8.compare(c5); + // Then + expect(comparison).toEqual(-1); + }); + }); - // Then - expect(comparison).toEqual(1); - }); + describe("|isNil|", () => { + it("should verify will return true if value is null or undefined, otherwise false", () => { + // When + const r1 = c1.isNil(); + const r2 = c2.isNil(); + const r3 = c3.isNil(); + const r4 = c4.isNil(); - it('should verify will return -1 for lessThan', () => { - // When - const comparison = c5.compare(c8); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeTrue(); + expect(r3).toBeFalse(); + expect(r4).toBeFalse(); + }); + }); - // Then - expect(comparison).toEqual(-1); - }); + describe("|notNil|", () => { + it("should verify will return false if value is null or undefined, otherwise true", () => { + // When + const r1 = c1.notNil(); + const r2 = c2.notNil(); + const r3 = c3.notNil(); + const r4 = c4.notNil(); - it('should verify will return -1 given comparable is not instance of the current Constructor', () => { - // Given - const c10 = new ComparableStub(v2); + // Then + expect(r1).toBeFalse(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + expect(r4).toBeTrue(); + }); + }); - // When - const comparison = c5.compare(c10); + describe("|like|", () => { + it("should verify will return true if values are similar, otherwise false", () => { + // When + const r1 = c3.like(c5); + const r2 = c3.like(c8); - // Then - expect(comparison).toEqual(-1); - }); - }); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + }); + }); - describe('|isNil|', () => { - it('should verify will return true if value is null or undefined, otherwise false', () => { - // When - const r1 = c1.isNil(); - const r2 = c2.isNil(); - const r3 = c3.isNil(); - const r4 = c4.isNil(); - - // Then - expect(r1).toBeTrue(); - expect(r2).toBeTrue(); - expect(r3).toBeFalse(); - expect(r4).toBeFalse(); - }); - }); + describe("|equal|", () => { + it("should verify will return true if values are equal, otherwise false", () => { + // When + const r1 = c3.equal(c5); + const r2 = c3.equal(c8); + const r3 = c4.equal(c7); + const r4 = c4.equal(c6); - describe('|notNil|', () => { - it('should verify will return false if value is null or undefined, otherwise true', () => { - // When - const r1 = c1.notNil(); - const r2 = c2.notNil(); - const r3 = c3.notNil(); - const r4 = c4.notNil(); - - // Then - expect(r1).toBeFalse(); - expect(r2).toBeFalse(); - expect(r3).toBeTrue(); - expect(r4).toBeTrue(); - }); - }); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + expect(r4).toBeFalse(); + }); + }); - describe('|like|', () => { - it('should verify will return true if values are similar, otherwise false', () => { - // When - const r1 = c3.like(c5); - const r2 = c3.like(c8); + describe("|notEqual|", () => { + it("should verify will return true if values are not equal, otherwise false", () => { + // When + const r1 = c3.notEqual(c5); + const r2 = c3.notEqual(c8); + const r3 = c4.notEqual(c7); + const r4 = c4.notEqual(c6); - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - }); - }); + // Then + expect(r1).toBeFalse(); + expect(r2).toBeTrue(); + expect(r3).toBeFalse(); + expect(r4).toBeTrue(); + }); + }); - describe('|equal|', () => { - it('should verify will return true if values are equal, otherwise false', () => { - // When - const r1 = c3.equal(c5); - const r2 = c3.equal(c8); - const r3 = c4.equal(c7); - const r4 = c4.equal(c6); - - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - expect(r3).toBeTrue(); - expect(r4).toBeFalse(); - }); - }); + describe("|lessThan|", () => { + it("should verify will return true if value is less than provided, otherwise false", () => { + // When + const r1 = c4.lessThan(c6); + const r2 = c6.lessThan(c4); - describe('|notEqual|', () => { - it('should verify will return true if values are not equal, otherwise false', () => { - // When - const r1 = c3.notEqual(c5); - const r2 = c3.notEqual(c8); - const r3 = c4.notEqual(c7); - const r4 = c4.notEqual(c6); - - // Then - expect(r1).toBeFalse(); - expect(r2).toBeTrue(); - expect(r3).toBeFalse(); - expect(r4).toBeTrue(); - }); - }); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + }); + }); - describe('|lessThan|', () => { - it('should verify will return true if value is less than provided, otherwise false', () => { - // When - const r1 = c4.lessThan(c6); - const r2 = c6.lessThan(c4); + describe("|lessThanInclusive|", () => { + it("should verify will return true if value is less than or equal to provided, otherwise false", () => { + // When + const r1 = c4.lessThanInclusive(c6); + const r2 = c6.lessThanInclusive(c4); + const r3 = c4.lessThanInclusive(c7); - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - }); - }); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + }); + }); - describe('|lessThanInclusive|', () => { - it('should verify will return true if value is less than or equal to provided, otherwise false', () => { - // When - const r1 = c4.lessThanInclusive(c6); - const r2 = c6.lessThanInclusive(c4); - const r3 = c4.lessThanInclusive(c7); - - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - expect(r3).toBeTrue(); - }); - }); + describe("|greaterThan|", () => { + it("should verify will return true if value is greater than provided, otherwise false", () => { + // When + const r1 = c6.greaterThan(c4); + const r2 = c4.greaterThan(c6); - describe('|greaterThan|', () => { - it('should verify will return true if value is greater than provided, otherwise false', () => { - // When - const r1 = c6.greaterThan(c4); - const r2 = c4.greaterThan(c6); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + }); + }); - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - }); - }); + describe("|greaterThanInclusive|", () => { + it("should verify will return true if value is greater than or equal to provided, otherwise false", () => { + // When + const r1 = c6.greaterThanInclusive(c4); + const r2 = c4.greaterThanInclusive(c6); + const r3 = c7.greaterThanInclusive(c4); - describe('|greaterThanInclusive|', () => { - it('should verify will return true if value is greater than or equal to provided, otherwise false', () => { - // When - const r1 = c6.greaterThanInclusive(c4); - const r2 = c4.greaterThanInclusive(c6); - const r3 = c7.greaterThanInclusive(c4); - - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - expect(r3).toBeTrue(); - }); - }); + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts index e325276015..e2121e1ad7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts @@ -5,106 +5,107 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Comparable } from '../../interfaces'; +import { Comparable } from "../../interfaces"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; /** * ** Comparable. */ export class ComparableImpl implements Comparable { - /** - * @inheritDoc - */ - public readonly value: T; - - /** - * ** Constructor. - */ - constructor(value: T) { - this.value = value; - } - - /** - * ** Factory method. - */ - static of(value: any): ComparableImpl { - return new ComparableImpl(value); - } - - /** - * @inheritDoc - */ - compare(comparable: Comparable): number { - if (comparable instanceof ComparableImpl) { - const evaluateSecondStatement = () => (this.value > comparable.value ? 1 : -1); - - return this.value === comparable.value ? 0 : evaluateSecondStatement(); - } else { - return -1; - } - } - - /** - * @inheritDoc - */ - isNil(): boolean { - return CollectionsUtil.isNil(this.value); - } - - /** - * @inheritDoc - */ - notNil(): boolean { - return CollectionsUtil.isDefined(this.value); - } - - /** - * @inheritDoc - */ - like(comparable: Comparable): boolean { - return this.compare(comparable) === 0; - } - - /** - * @inheritDoc - */ - equal(comparable: Comparable): boolean { - return this.compare(comparable) === 0; - } - - /** - * @inheritDoc - */ - notEqual(comparable: Comparable): boolean { - return this.compare(comparable) !== 0; - } - - /** - * @inheritDoc - */ - lessThan(comparable: Comparable): boolean { - return this.compare(comparable) < 0; - } - - /** - * @inheritDoc - */ - lessThanInclusive(comparable: Comparable): boolean { - return this.compare(comparable) <= 0; - } - - /** - * @inheritDoc - */ - greaterThan(comparable: Comparable): boolean { - return this.compare(comparable) > 0; - } - - /** - * @inheritDoc - */ - greaterThanInclusive(comparable: Comparable): boolean { - return this.compare(comparable) >= 0; + /** + * @inheritDoc + */ + public readonly value: T; + + /** + * ** Constructor. + */ + constructor(value: T) { + this.value = value; + } + + /** + * ** Factory method. + */ + static of(value: any): ComparableImpl { + return new ComparableImpl(value); + } + + /** + * @inheritDoc + */ + compare(comparable: Comparable): number { + if (comparable instanceof ComparableImpl) { + const evaluateSecondStatement = () => + this.value > comparable.value ? 1 : -1; + + return this.value === comparable.value ? 0 : evaluateSecondStatement(); + } else { + return -1; } + } + + /** + * @inheritDoc + */ + isNil(): boolean { + return CollectionsUtil.isNil(this.value); + } + + /** + * @inheritDoc + */ + notNil(): boolean { + return CollectionsUtil.isDefined(this.value); + } + + /** + * @inheritDoc + */ + like(comparable: Comparable): boolean { + return this.compare(comparable) === 0; + } + + /** + * @inheritDoc + */ + equal(comparable: Comparable): boolean { + return this.compare(comparable) === 0; + } + + /** + * @inheritDoc + */ + notEqual(comparable: Comparable): boolean { + return this.compare(comparable) !== 0; + } + + /** + * @inheritDoc + */ + lessThan(comparable: Comparable): boolean { + return this.compare(comparable) < 0; + } + + /** + * @inheritDoc + */ + lessThanInclusive(comparable: Comparable): boolean { + return this.compare(comparable) <= 0; + } + + /** + * @inheritDoc + */ + greaterThan(comparable: Comparable): boolean { + return this.compare(comparable) > 0; + } + + /** + * @inheritDoc + */ + greaterThanInclusive(comparable: Comparable): boolean { + return this.compare(comparable) >= 0; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts index 4b2bcd7ca2..c50c138148 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './comparable.impl'; -export * from './predicates-comparable.impl'; +export * from "./comparable.impl"; +export * from "./predicates-comparable.impl"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts index 9a0a6af84e..82da2c9793 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts @@ -3,316 +3,401 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -import { PredicatesComparable } from './predicates-comparable.impl'; +import { PredicatesComparable } from "./predicates-comparable.impl"; class ComparableStub implements Comparable { - public readonly value: any; + public readonly value: any; - constructor(value: any) { - this.value = value; - } + constructor(value: any) { + this.value = value; + } - compare(_comparable: Comparable): number { - return 0; - } + compare(_comparable: Comparable): number { + return 0; + } - equal(_comparable: Comparable): boolean { - return true; - } + equal(_comparable: Comparable): boolean { + return true; + } - like(_comparable: Comparable): boolean { - return true; - } + like(_comparable: Comparable): boolean { + return true; + } - notEqual(_comparable: Comparable): boolean { - return false; - } + notEqual(_comparable: Comparable): boolean { + return false; + } - isNil(): boolean { - return false; - } + isNil(): boolean { + return false; + } - notNil(): boolean { - return true; - } + notNil(): boolean { + return true; + } - greaterThan(_comparable: Comparable): boolean { - return false; - } + greaterThan(_comparable: Comparable): boolean { + return false; + } - greaterThanInclusive(_comparable: Comparable): boolean { - return false; - } + greaterThanInclusive(_comparable: Comparable): boolean { + return false; + } - lessThan(_comparable: Comparable): boolean { - return false; - } + lessThan(_comparable: Comparable): boolean { + return false; + } - lessThanInclusive(_comparable: Comparable): boolean { - return false; - } + lessThanInclusive(_comparable: Comparable): boolean { + return false; + } } class PredicateEqualStub implements Predicate { - comparable: ComparableStub; + comparable: ComparableStub; - constructor(comparable: ComparableStub) { - this.comparable = comparable; - } + constructor(comparable: ComparableStub) { + this.comparable = comparable; + } - evaluate(comparable: Comparable): boolean { - return this.comparable.equal(comparable); - } + evaluate(comparable: Comparable): boolean { + return this.comparable.equal(comparable); + } } -describe('PredicatesComparable', () => { - let predicate1: Predicate; - let predicate2: Predicate; - let predicate3: Predicate; +describe("PredicatesComparable", () => { + let predicate1: Predicate; + let predicate2: Predicate; + let predicate3: Predicate; + + beforeEach(() => { + predicate1 = new PredicateEqualStub(new ComparableStub("A")); + predicate2 = new PredicateEqualStub(new ComparableStub("B")); + predicate3 = new PredicateEqualStub(new ComparableStub("1")); + }); + + it("should verify instance is created", () => { + // When + const instance = new PredicatesComparable(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + }); + + it("should verify value is correctly assigned", () => { + // When + const instance = new PredicatesComparable(predicate1, predicate2); + + // Then + expect(instance.value[0]).toBe(predicate1); + expect(instance.value[1]).toBe(predicate2); + }); + + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = PredicatesComparable.of(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(PredicatesComparable); + expect(instance.value[0]).toBe(predicate1); + expect(instance.value[1]).toBe(predicate2); + }); + }); + }); + }); + + describe("Methods::()", () => { + let predicatesComparable: PredicatesComparable; + let injectedComparable: ComparableStub; + let consoleWarnSpy: jasmine.Spy; beforeEach(() => { - predicate1 = new PredicateEqualStub(new ComparableStub('A')); - predicate2 = new PredicateEqualStub(new ComparableStub('B')); - predicate3 = new PredicateEqualStub(new ComparableStub('1')); + predicatesComparable = PredicatesComparable.of(predicate1, predicate2); + injectedComparable = new ComparableStub("C"); + consoleWarnSpy = spyOn(console, "warn").and.callFake(CallFake); }); - it('should verify instance is created', () => { + describe("|compare|", () => { + it("should verify will return -1 and log warn to console", () => { // When - const instance = new PredicatesComparable(predicate1, predicate2); + const comparison = predicatesComparable.compare(injectedComparable); // Then - expect(instance).toBeDefined(); + expect(comparison).toEqual(-1); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "PredicatesComparable, unsupported comparison.", + ); + }); }); - it('should verify value is correctly assigned', () => { + describe("|isNil|", () => { + it("should verify will return false all the time", () => { + // Given + const c1 = PredicatesComparable.of(null); + const c2 = PredicatesComparable.of(undefined); + const c3 = PredicatesComparable.of(predicate1); + // When - const instance = new PredicatesComparable(predicate1, predicate2); + const r1 = c1.isNil(); + const r2 = c2.isNil(); + const r3 = c3.isNil(); // Then - expect(instance.value[0]).toBe(predicate1); - expect(instance.value[1]).toBe(predicate2); + expect(r1).toBeFalse(); + expect(r2).toBeFalse(); + expect(r3).toBeFalse(); + }); }); - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = PredicatesComparable.of(predicate1, predicate2); - - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(PredicatesComparable); - expect(instance.value[0]).toBe(predicate1); - expect(instance.value[1]).toBe(predicate2); - }); - }); - }); + describe("|notNil|", () => { + it("should verify will return true all the time", () => { + // Given + const c1 = PredicatesComparable.of(null); + const c2 = PredicatesComparable.of(undefined); + const c3 = PredicatesComparable.of(predicate1); + + // When + const r1 = c1.notNil(); + const r2 = c2.notNil(); + const r3 = c3.notNil(); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeTrue(); + expect(r3).toBeTrue(); + }); }); - describe('Methods::()', () => { - let predicatesComparable: PredicatesComparable; - let injectedComparable: ComparableStub; - let consoleWarnSpy: jasmine.Spy; + describe("|like|", () => { + it("should verify will return true if one or more predicates return true, otherwise false", () => { + // Given + const predicate1EvaluateSpy = spyOn( + predicate1, + "evaluate", + ).and.returnValues(false, false); + const predicate2EvaluateSpy = spyOn( + predicate2, + "evaluate", + ).and.returnValues(true, false); + const predicate3EvaluateSpy = spyOn( + predicate3, + "evaluate", + ).and.returnValues(false); + const comparableStub = new ComparableStub("D"); + predicatesComparable = PredicatesComparable.of( + predicate1, + predicate2, + predicate3, + ); - beforeEach(() => { - predicatesComparable = PredicatesComparable.of(predicate1, predicate2); - injectedComparable = new ComparableStub('C'); - consoleWarnSpy = spyOn(console, 'warn').and.callFake(CallFake); - }); + // When + const r1 = predicatesComparable.like(injectedComparable); + const r2 = predicatesComparable.like(comparableStub); - describe('|compare|', () => { - it('should verify will return -1 and log warn to console', () => { - // When - const comparison = predicatesComparable.compare(injectedComparable); + // Then + expect(r1).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([ + injectedComparable, + ]); + expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([ + injectedComparable, + ]); + + expect(r2).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([ + comparableStub, + ]); + expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([ + comparableStub, + ]); + expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([ + comparableStub, + ]); + + expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); + }); + }); - // Then - expect(comparison).toEqual(-1); - expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); - }); - }); + describe("|equal|", () => { + it("should verify will return true if all predicates return true, otherwise false", () => { + // Given + const predicate1EvaluateSpy = spyOn( + predicate1, + "evaluate", + ).and.returnValues(true, false, true); + const predicate2EvaluateSpy = spyOn( + predicate2, + "evaluate", + ).and.returnValues(false, true); + const predicate3EvaluateSpy = spyOn( + predicate3, + "evaluate", + ).and.returnValues(true); + const comparableStub1 = new ComparableStub("F"); + const comparableStub2 = new ComparableStub("G"); + predicatesComparable = PredicatesComparable.of( + predicate1, + predicate2, + predicate3, + ); - describe('|isNil|', () => { - it('should verify will return false all the time', () => { - // Given - const c1 = PredicatesComparable.of(null); - const c2 = PredicatesComparable.of(undefined); - const c3 = PredicatesComparable.of(predicate1); - - // When - const r1 = c1.isNil(); - const r2 = c2.isNil(); - const r3 = c3.isNil(); - - // Then - expect(r1).toBeFalse(); - expect(r2).toBeFalse(); - expect(r3).toBeFalse(); - }); - }); + // When + const r1 = predicatesComparable.equal(injectedComparable); + const r2 = predicatesComparable.equal(comparableStub1); + const r3 = predicatesComparable.equal(comparableStub2); - describe('|notNil|', () => { - it('should verify will return true all the time', () => { - // Given - const c1 = PredicatesComparable.of(null); - const c2 = PredicatesComparable.of(undefined); - const c3 = PredicatesComparable.of(predicate1); - - // When - const r1 = c1.notNil(); - const r2 = c2.notNil(); - const r3 = c3.notNil(); - - // Then - expect(r1).toBeTrue(); - expect(r2).toBeTrue(); - expect(r3).toBeTrue(); - }); - }); + // Then + expect(r1).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([ + injectedComparable, + ]); + expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([ + injectedComparable, + ]); + + expect(r2).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([ + comparableStub1, + ]); + + expect(r3).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(2)).toEqual([ + comparableStub2, + ]); + expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([ + comparableStub2, + ]); + expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([ + comparableStub2, + ]); + + expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(3); + expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); + }); + }); - describe('|like|', () => { - it('should verify will return true if one or more predicates return true, otherwise false', () => { - // Given - const predicate1EvaluateSpy = spyOn(predicate1, 'evaluate').and.returnValues(false, false); - const predicate2EvaluateSpy = spyOn(predicate2, 'evaluate').and.returnValues(true, false); - const predicate3EvaluateSpy = spyOn(predicate3, 'evaluate').and.returnValues(false); - const comparableStub = new ComparableStub('D'); - predicatesComparable = PredicatesComparable.of(predicate1, predicate2, predicate3); - - // When - const r1 = predicatesComparable.like(injectedComparable); - const r2 = predicatesComparable.like(comparableStub); - - // Then - expect(r1).toBeTrue(); - expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); - expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); - - expect(r2).toBeFalse(); - expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub]); - expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub]); - expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub]); - - expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(2); - expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); - expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); - }); - }); + describe("|notEqual|", () => { + it("should verify will return true if one or more predicates return false, otherwise false", () => { + // Given + const predicate1EvaluateSpy = spyOn( + predicate1, + "evaluate", + ).and.returnValues(false, true, true); + const predicate2EvaluateSpy = spyOn( + predicate2, + "evaluate", + ).and.returnValues(false, true); + const predicate3EvaluateSpy = spyOn( + predicate3, + "evaluate", + ).and.returnValues(true); + const comparableStub1 = new ComparableStub("H"); + const comparableStub2 = new ComparableStub("I"); + predicatesComparable = PredicatesComparable.of( + predicate1, + predicate2, + predicate3, + ); - describe('|equal|', () => { - it('should verify will return true if all predicates return true, otherwise false', () => { - // Given - const predicate1EvaluateSpy = spyOn(predicate1, 'evaluate').and.returnValues(true, false, true); - const predicate2EvaluateSpy = spyOn(predicate2, 'evaluate').and.returnValues(false, true); - const predicate3EvaluateSpy = spyOn(predicate3, 'evaluate').and.returnValues(true); - const comparableStub1 = new ComparableStub('F'); - const comparableStub2 = new ComparableStub('G'); - predicatesComparable = PredicatesComparable.of(predicate1, predicate2, predicate3); - - // When - const r1 = predicatesComparable.equal(injectedComparable); - const r2 = predicatesComparable.equal(comparableStub1); - const r3 = predicatesComparable.equal(comparableStub2); - - // Then - expect(r1).toBeFalse(); - expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); - expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); - - expect(r2).toBeFalse(); - expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub1]); - - expect(r3).toBeTrue(); - expect(predicate1EvaluateSpy.calls.argsFor(2)).toEqual([comparableStub2]); - expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub2]); - expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub2]); - - expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(3); - expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); - expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); - }); - }); + // When + const r1 = predicatesComparable.notEqual(injectedComparable); + const r2 = predicatesComparable.notEqual(comparableStub1); + const r3 = predicatesComparable.notEqual(comparableStub2); - describe('|notEqual|', () => { - it('should verify will return true if one or more predicates return false, otherwise false', () => { - // Given - const predicate1EvaluateSpy = spyOn(predicate1, 'evaluate').and.returnValues(false, true, true); - const predicate2EvaluateSpy = spyOn(predicate2, 'evaluate').and.returnValues(false, true); - const predicate3EvaluateSpy = spyOn(predicate3, 'evaluate').and.returnValues(true); - const comparableStub1 = new ComparableStub('H'); - const comparableStub2 = new ComparableStub('I'); - predicatesComparable = PredicatesComparable.of(predicate1, predicate2, predicate3); - - // When - const r1 = predicatesComparable.notEqual(injectedComparable); - const r2 = predicatesComparable.notEqual(comparableStub1); - const r3 = predicatesComparable.notEqual(comparableStub2); - - // Then - expect(r1).toBeTrue(); - expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); - - expect(r2).toBeTrue(); - expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub1]); - expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub1]); - - expect(r3).toBeFalse(); - expect(predicate1EvaluateSpy.calls.argsFor(2)).toEqual([comparableStub2]); - expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub2]); - expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub2]); - - expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(3); - expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); - expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); - }); - }); + // Then + expect(r1).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([ + injectedComparable, + ]); + + expect(r2).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([ + comparableStub1, + ]); + expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([ + comparableStub1, + ]); + + expect(r3).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(2)).toEqual([ + comparableStub2, + ]); + expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([ + comparableStub2, + ]); + expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([ + comparableStub2, + ]); + + expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(3); + expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); + }); + }); - describe('|lessThan|', () => { - it('should verify will return -1 and log warn to console', () => { - // When - const comparison = predicatesComparable.lessThan(injectedComparable); + describe("|lessThan|", () => { + it("should verify will return -1 and log warn to console", () => { + // When + const comparison = predicatesComparable.lessThan(injectedComparable); - // Then - expect(comparison).toBeFalse(); - expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); - }); - }); + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "PredicatesComparable, unsupported comparison.", + ); + }); + }); - describe('|lessThanInclusive|', () => { - it('should verify will return -1 and log warn to console', () => { - // When - const comparison = predicatesComparable.lessThanInclusive(injectedComparable); + describe("|lessThanInclusive|", () => { + it("should verify will return -1 and log warn to console", () => { + // When + const comparison = + predicatesComparable.lessThanInclusive(injectedComparable); - // Then - expect(comparison).toBeFalse(); - expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); - }); - }); + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "PredicatesComparable, unsupported comparison.", + ); + }); + }); - describe('|greaterThan|', () => { - it('should verify will return -1 and log warn to console', () => { - // When - const comparison = predicatesComparable.greaterThan(injectedComparable); + describe("|greaterThan|", () => { + it("should verify will return -1 and log warn to console", () => { + // When + const comparison = predicatesComparable.greaterThan(injectedComparable); - // Then - expect(comparison).toBeFalse(); - expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); - }); - }); + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "PredicatesComparable, unsupported comparison.", + ); + }); + }); - describe('|greaterThanInclusive|', () => { - it('should verify will return -1 and log warn to console', () => { - // When - const comparison = predicatesComparable.greaterThanInclusive(injectedComparable); + describe("|greaterThanInclusive|", () => { + it("should verify will return -1 and log warn to console", () => { + // When + const comparison = + predicatesComparable.greaterThanInclusive(injectedComparable); - // Then - expect(comparison).toBeFalse(); - expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); - }); - }); + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "PredicatesComparable, unsupported comparison.", + ); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts index 11a7caca1d..60051475bf 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts @@ -3,106 +3,108 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../utils'; - -import { Comparable, Predicate } from '../../interfaces'; - -export class PredicatesComparable implements Comparable { - /** - * @inheritDoc - */ - public readonly value: T; - - /** - * ** Constructor. - */ - constructor(...predicates: T) { - this.value = predicates ?? ([] as T); - } - - /** - * ** Factory method. - */ - static of(...predicates: Predicate[]): PredicatesComparable { - return new PredicatesComparable(...predicates); - } - - /** - * @inheritDoc - */ - compare(_comparable: Comparable): number { - console.warn('PredicatesComparable, unsupported comparison.'); - - return -1; - } - - /** - * @inheritDoc - */ - isNil(): boolean { - return CollectionsUtil.isNil(this.value); - } - - /** - * @inheritDoc - */ - notNil(): boolean { - return CollectionsUtil.isDefined(this.value); - } - - /** - * @inheritDoc - */ - like(comparable: Comparable): boolean { - return this.value.some((predicate) => predicate.evaluate(comparable)); - } - - /** - * @inheritDoc - */ - equal(comparable: Comparable): boolean { - return this.value.every((predicate) => predicate.evaluate(comparable)); - } - - /** - * @inheritDoc - */ - notEqual(comparable: Comparable): boolean { - return !this.value.every((predicate) => predicate.evaluate(comparable)); - } - - /** - * @inheritDoc - */ - lessThan(_comparable: Comparable): boolean { - return PredicatesComparable._defaultUnsupported(); - } - - /** - * @inheritDoc - */ - lessThanInclusive(_comparable: Comparable): boolean { - return PredicatesComparable._defaultUnsupported(); - } - - /** - * @inheritDoc - */ - greaterThan(_comparable: Comparable): boolean { - return PredicatesComparable._defaultUnsupported(); - } - - /** - * @inheritDoc - */ - greaterThanInclusive(_comparable: Comparable): boolean { - return PredicatesComparable._defaultUnsupported(); - } - - // eslint-disable-next-line @typescript-eslint/member-ordering - private static _defaultUnsupported(): boolean { - console.warn('PredicatesComparable, unsupported comparison.'); - - return false; - } +import { CollectionsUtil } from "../../../utils"; + +import { Comparable, Predicate } from "../../interfaces"; + +export class PredicatesComparable< + T extends Predicate[] = Predicate[], +> implements Comparable { + /** + * @inheritDoc + */ + public readonly value: T; + + /** + * ** Constructor. + */ + constructor(...predicates: T) { + this.value = predicates ?? ([] as T); + } + + /** + * ** Factory method. + */ + static of(...predicates: Predicate[]): PredicatesComparable { + return new PredicatesComparable(...predicates); + } + + /** + * @inheritDoc + */ + compare(_comparable: Comparable): number { + console.warn("PredicatesComparable, unsupported comparison."); + + return -1; + } + + /** + * @inheritDoc + */ + isNil(): boolean { + return CollectionsUtil.isNil(this.value); + } + + /** + * @inheritDoc + */ + notNil(): boolean { + return CollectionsUtil.isDefined(this.value); + } + + /** + * @inheritDoc + */ + like(comparable: Comparable): boolean { + return this.value.some((predicate) => predicate.evaluate(comparable)); + } + + /** + * @inheritDoc + */ + equal(comparable: Comparable): boolean { + return this.value.every((predicate) => predicate.evaluate(comparable)); + } + + /** + * @inheritDoc + */ + notEqual(comparable: Comparable): boolean { + return !this.value.every((predicate) => predicate.evaluate(comparable)); + } + + /** + * @inheritDoc + */ + lessThan(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + /** + * @inheritDoc + */ + lessThanInclusive(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + /** + * @inheritDoc + */ + greaterThan(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + /** + * @inheritDoc + */ + greaterThanInclusive(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + private static _defaultUnsupported(): boolean { + console.warn("PredicatesComparable, unsupported comparison."); + + return false; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts index bfe6f7b34f..a822bf7313 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts @@ -3,149 +3,155 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CallFake } from '../../../unit-testing'; - -import { Comparable, Predicate } from '../../interfaces'; - -import { PredicatesComparable } from '../comparable'; - -import { CompoundPredicate } from './base-compound.predicate'; -import { And } from './and.predicate'; - -describe('And', () => { - it('should verify instance is created from one predicate', () => { - // Given - const predicate1: Predicate = { +import { CallFake } from "../../../unit-testing"; + +import { Comparable, Predicate } from "../../interfaces"; + +import { PredicatesComparable } from "../comparable"; + +import { CompoundPredicate } from "./base-compound.predicate"; +import { And } from "./and.predicate"; + +describe("And", () => { + it("should verify instance is created from one predicate", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + + // When + const instance = new And(predicate1); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + }); + + it("should verify instance is created from predicates", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + + // When + const instance = new And(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); + + it("should verify instance is created from comparable", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const comparable = new PredicatesComparable(predicate1, predicate2); + + // When + const instance = new And(comparable); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance from predicates", () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, - evaluate: CallFake - }; - - // When - const instance = new And(predicate1); - - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBeInstanceOf(PredicatesComparable); - expect(instance.comparable.value[0]).toBe(predicate1); - }); + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + + // When + const instance = And.of(predicate1, predicate2); + + // Then + expect(instance).toBeInstanceOf(And); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); - it('should verify instance is created from predicates', () => { - // Given - const predicate1: Predicate = { + it("should verify factory method will create instance from comparable", () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { + evaluate: CallFake, + }; + const predicate2: Predicate = { comparable: {} as Comparable, - evaluate: CallFake - }; + evaluate: CallFake, + }; + const comparable = new PredicatesComparable(predicate1, predicate2); - // When - const instance = new And(predicate1, predicate2); + // When + const instance = And.of(comparable); - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBeInstanceOf(PredicatesComparable); - expect(instance.comparable.value[0]).toBe(predicate1); - expect(instance.comparable.value[1]).toBe(predicate2); + // Then + expect(instance).toBeInstanceOf(And); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + }); }); + }); - it('should verify instance is created from comparable', () => { + describe("Methods::", () => { + describe("|evaluate|", () => { + it("should verify will invoke correct methods", () => { // Given + const comparableEqualSpy = spyOn( + PredicatesComparable.prototype, + "equal", + ).and.returnValue(true); const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake + comparable: {} as Comparable, + evaluate: CallFake, }; const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake + comparable: {} as Comparable, + evaluate: CallFake, }; - const comparable = new PredicatesComparable(predicate1, predicate2); + const predicatesComparable = new PredicatesComparable( + predicate1, + predicate2, + ); + const instance = new And(predicatesComparable); + const standardComparable = {} as Comparable; // When - const instance = new And(comparable); + const result = instance.evaluate(standardComparable); // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBe(comparable); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance from predicates', () => { - // Given - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - - // When - const instance = And.of(predicate1, predicate2); - - // Then - expect(instance).toBeInstanceOf(And); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBeInstanceOf(PredicatesComparable); - expect(instance.comparable.value[0]).toBe(predicate1); - expect(instance.comparable.value[1]).toBe(predicate2); - }); - - it('should verify factory method will create instance from comparable', () => { - // Given - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const comparable = new PredicatesComparable(predicate1, predicate2); - - // When - const instance = And.of(comparable); - - // Then - expect(instance).toBeInstanceOf(And); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBe(comparable); - }); - }); - }); - }); - - describe('Methods::', () => { - describe('|evaluate|', () => { - it('should verify will invoke correct methods', () => { - // Given - const comparableEqualSpy = spyOn(PredicatesComparable.prototype, 'equal').and.returnValue(true); - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicatesComparable = new PredicatesComparable(predicate1, predicate2); - const instance = new And(predicatesComparable); - const standardComparable = {} as Comparable; - - // When - const result = instance.evaluate(standardComparable); - - // Then - expect(result).toBeTrue(); - expect(comparableEqualSpy).toHaveBeenCalledWith(standardComparable); - }); - }); + expect(result).toBeTrue(); + expect(comparableEqualSpy).toHaveBeenCalledWith(standardComparable); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts index dd2218e418..bb1d249124 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts @@ -3,30 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -import { PredicatesComparable } from '../comparable'; +import { PredicatesComparable } from "../comparable"; -import { CompoundPredicate } from './base-compound.predicate'; +import { CompoundPredicate } from "./base-compound.predicate"; export class And extends CompoundPredicate { - /** - * ** Factory method. - */ - static override of(comparable: PredicatesComparable): And; - static override of(...predicates: Predicate[]): And; - static override of(...values: Predicate[] | [PredicatesComparable]): And { - if (values[0] instanceof PredicatesComparable) { - return new And(values[0]); - } else { - return new And(...(values as Predicate[])); - } + /** + * ** Factory method. + */ + static override of(comparable: PredicatesComparable): And; + static override of(...predicates: Predicate[]): And; + static override of(...values: Predicate[] | [PredicatesComparable]): And { + if (values[0] instanceof PredicatesComparable) { + return new And(values[0]); + } else { + return new And(...(values as Predicate[])); } + } - /** - * @inheritDoc - */ - evaluate(comparable: Comparable): boolean { - return this.comparable.equal(comparable); - } + /** + * @inheritDoc + */ + evaluate(comparable: Comparable): boolean { + return this.comparable.equal(comparable); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts index e1ca879f76..3139839969 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts @@ -3,29 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -import { CompoundPredicate } from './base-compound.predicate'; +import { CompoundPredicate } from "./base-compound.predicate"; -describe('CompoundPredicate', () => { - describe('Statics::', () => { - describe('|of|', () => { - it('should verify will throw Error', () => { - // Given - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; +describe("CompoundPredicate", () => { + describe("Statics::", () => { + describe("|of|", () => { + it("should verify will throw Error", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; - // When/Then - expect(() => CompoundPredicate.of(predicate1, predicate2)).toThrowError('Method have to be overridden in Subclasses.'); - }); - }); + // When/Then + expect(() => CompoundPredicate.of(predicate1, predicate2)).toThrowError( + "Method have to be overridden in Subclasses.", + ); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts index 5e99910a10..cff0a24013 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts @@ -3,42 +3,45 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -import { PredicatesComparable } from '../comparable'; +import { PredicatesComparable } from "../comparable"; -export abstract class CompoundPredicate implements Predicate { - /** - * @inheritDoc - */ - readonly comparable: PredicatesComparable; +export abstract class CompoundPredicate implements Predicate< + PredicatesComparable, + Comparable +> { + /** + * @inheritDoc + */ + readonly comparable: PredicatesComparable; - /** - * ** Constructor. - */ - constructor(comparable: PredicatesComparable); - constructor(...predicates: Predicate[]); - constructor(...values: Predicate[] | [PredicatesComparable]) { - if (values.length === 1) { - if (values[0] instanceof PredicatesComparable) { - this.comparable = values[0]; - } else { - this.comparable = PredicatesComparable.of(values[0]); - } - } else { - this.comparable = PredicatesComparable.of(...(values as Predicate[])); - } + /** + * ** Constructor. + */ + constructor(comparable: PredicatesComparable); + constructor(...predicates: Predicate[]); + constructor(...values: Predicate[] | [PredicatesComparable]) { + if (values.length === 1) { + if (values[0] instanceof PredicatesComparable) { + this.comparable = values[0]; + } else { + this.comparable = PredicatesComparable.of(values[0]); + } + } else { + this.comparable = PredicatesComparable.of(...(values as Predicate[])); } + } - /** - * ** Factory method. - */ - static of(..._args: unknown[]): CompoundPredicate { - throw new Error('Method have to be overridden in Subclasses.'); - } + /** + * ** Factory method. + */ + static of(..._args: unknown[]): CompoundPredicate { + throw new Error("Method have to be overridden in Subclasses."); + } - /** - * @inheritDoc - */ - abstract evaluate(value: Comparable): boolean; + /** + * @inheritDoc + */ + abstract evaluate(value: Comparable): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts index 116634097f..56f1bec5e6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './base-compound.predicate'; -export * from './and.predicate'; -export * from './or.predicate'; +export * from "./base-compound.predicate"; +export * from "./and.predicate"; +export * from "./or.predicate"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts index 3918ded26a..2d5cc09e5f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts @@ -3,149 +3,155 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CallFake } from '../../../unit-testing'; - -import { Comparable, Predicate } from '../../interfaces'; - -import { PredicatesComparable } from '../comparable'; - -import { CompoundPredicate } from './base-compound.predicate'; -import { Or } from './or.predicate'; - -describe('Or', () => { - it('should verify instance is created from one predicate', () => { - // Given - const predicate1: Predicate = { +import { CallFake } from "../../../unit-testing"; + +import { Comparable, Predicate } from "../../interfaces"; + +import { PredicatesComparable } from "../comparable"; + +import { CompoundPredicate } from "./base-compound.predicate"; +import { Or } from "./or.predicate"; + +describe("Or", () => { + it("should verify instance is created from one predicate", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + + // When + const instance = new Or(predicate1); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + }); + + it("should verify instance is created from predicates", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + + // When + const instance = new Or(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); + + it("should verify instance is created from comparable", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const comparable = new PredicatesComparable(predicate1, predicate2); + + // When + const instance = new Or(comparable); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance from predicates", () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, - evaluate: CallFake - }; - - // When - const instance = new Or(predicate1); - - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBeInstanceOf(PredicatesComparable); - expect(instance.comparable.value[0]).toBe(predicate1); - }); + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + + // When + const instance = Or.of(predicate1, predicate2); + + // Then + expect(instance).toBeInstanceOf(Or); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); - it('should verify instance is created from predicates', () => { - // Given - const predicate1: Predicate = { + it("should verify factory method will create instance from comparable", () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { + evaluate: CallFake, + }; + const predicate2: Predicate = { comparable: {} as Comparable, - evaluate: CallFake - }; + evaluate: CallFake, + }; + const comparable = new PredicatesComparable(predicate1, predicate2); - // When - const instance = new Or(predicate1, predicate2); + // When + const instance = Or.of(comparable); - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBeInstanceOf(PredicatesComparable); - expect(instance.comparable.value[0]).toBe(predicate1); - expect(instance.comparable.value[1]).toBe(predicate2); + // Then + expect(instance).toBeInstanceOf(Or); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + }); }); + }); - it('should verify instance is created from comparable', () => { + describe("Methods::", () => { + describe("|evaluate|", () => { + it("should verify will invoke correct methods", () => { // Given + const comparableLikeSpy = spyOn( + PredicatesComparable.prototype, + "like", + ).and.returnValue(true); const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake + comparable: {} as Comparable, + evaluate: CallFake, }; const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake + comparable: {} as Comparable, + evaluate: CallFake, }; - const comparable = new PredicatesComparable(predicate1, predicate2); + const predicatesComparable = new PredicatesComparable( + predicate1, + predicate2, + ); + const instance = new Or(predicatesComparable); + const standardComparable = {} as Comparable; // When - const instance = new Or(comparable); + const result = instance.evaluate(standardComparable); // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBe(comparable); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance from predicates', () => { - // Given - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - - // When - const instance = Or.of(predicate1, predicate2); - - // Then - expect(instance).toBeInstanceOf(Or); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBeInstanceOf(PredicatesComparable); - expect(instance.comparable.value[0]).toBe(predicate1); - expect(instance.comparable.value[1]).toBe(predicate2); - }); - - it('should verify factory method will create instance from comparable', () => { - // Given - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const comparable = new PredicatesComparable(predicate1, predicate2); - - // When - const instance = Or.of(comparable); - - // Then - expect(instance).toBeInstanceOf(Or); - expect(instance).toBeInstanceOf(CompoundPredicate); - expect(instance.comparable).toBe(comparable); - }); - }); - }); - }); - - describe('Methods::', () => { - describe('|evaluate|', () => { - it('should verify will invoke correct methods', () => { - // Given - const comparableLikeSpy = spyOn(PredicatesComparable.prototype, 'like').and.returnValue(true); - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicatesComparable = new PredicatesComparable(predicate1, predicate2); - const instance = new Or(predicatesComparable); - const standardComparable = {} as Comparable; - - // When - const result = instance.evaluate(standardComparable); - - // Then - expect(result).toBeTrue(); - expect(comparableLikeSpy).toHaveBeenCalledWith(standardComparable); - }); - }); + expect(result).toBeTrue(); + expect(comparableLikeSpy).toHaveBeenCalledWith(standardComparable); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts index b6e99a15a9..dcc01cf763 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts @@ -3,30 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -import { PredicatesComparable } from '../comparable'; +import { PredicatesComparable } from "../comparable"; -import { CompoundPredicate } from './base-compound.predicate'; +import { CompoundPredicate } from "./base-compound.predicate"; export class Or extends CompoundPredicate { - /** - * ** Factory method. - */ - static override of(comparable: PredicatesComparable): Or; - static override of(...predicates: Predicate[]): Or; - static override of(...values: Predicate[] | [PredicatesComparable]): Or { - if (values[0] instanceof PredicatesComparable) { - return new Or(values[0]); - } else { - return new Or(...(values as Predicate[])); - } + /** + * ** Factory method. + */ + static override of(comparable: PredicatesComparable): Or; + static override of(...predicates: Predicate[]): Or; + static override of(...values: Predicate[] | [PredicatesComparable]): Or { + if (values[0] instanceof PredicatesComparable) { + return new Or(values[0]); + } else { + return new Or(...(values as Predicate[])); } + } - /** - * @inheritDoc - */ - evaluate(comparable: Comparable): boolean { - return this.comparable.like(comparable); - } + /** + * @inheritDoc + */ + evaluate(comparable: Comparable): boolean { + return this.comparable.like(comparable); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts index 6bf766e4b1..519235e3d0 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './comparable'; -export * from './compound'; -export * from './simple'; +export * from "./comparable"; +export * from "./compound"; +export * from "./simple"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts index 80c990b52b..a3156fcae9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts @@ -3,29 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -import { SimplePredicate } from './base-simple.predicate'; +import { SimplePredicate } from "./base-simple.predicate"; -describe('SimplePredicate', () => { - describe('Statics::', () => { - describe('|of|', () => { - it('should verify will throw Error', () => { - // Given - const predicate1: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; - const predicate2: Predicate = { - comparable: {} as Comparable, - evaluate: CallFake - }; +describe("SimplePredicate", () => { + describe("Statics::", () => { + describe("|of|", () => { + it("should verify will throw Error", () => { + // Given + const predicate1: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; + const predicate2: Predicate = { + comparable: {} as Comparable, + evaluate: CallFake, + }; - // When/Then - expect(() => SimplePredicate.of(predicate1, predicate2)).toThrowError('Method have to be overridden in Subclasses.'); - }); - }); + // When/Then + expect(() => SimplePredicate.of(predicate1, predicate2)).toThrowError( + "Method have to be overridden in Subclasses.", + ); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts index 4e032227c0..0422517e69 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts @@ -3,30 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, Predicate } from '../../interfaces'; +import { Comparable, Predicate } from "../../interfaces"; -export abstract class SimplePredicate implements Predicate { - /** - * @inheritDoc - */ - readonly comparable: T; +export abstract class SimplePredicate< + T extends Comparable = Comparable, +> implements Predicate { + /** + * @inheritDoc + */ + readonly comparable: T; - /** - * ** Constructor. - */ - protected constructor(comparable: T) { - this.comparable = comparable; - } + /** + * ** Constructor. + */ + protected constructor(comparable: T) { + this.comparable = comparable; + } - /** - * ** Factory method. - */ - static of(..._args: unknown[]): SimplePredicate { - throw new Error('Method have to be overridden in Subclasses.'); - } + /** + * ** Factory method. + */ + static of(..._args: unknown[]): SimplePredicate { + throw new Error("Method have to be overridden in Subclasses."); + } - /** - * @inheritDoc - */ - abstract evaluate(comparable: T): boolean; + /** + * @inheritDoc + */ + abstract evaluate(comparable: T): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts index 542016eabf..9357d37666 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts @@ -3,78 +3,78 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable } from '../../interfaces'; +import { Comparable } from "../../interfaces"; -import { ComparableImpl } from '../comparable'; +import { ComparableImpl } from "../comparable"; -import { Equal } from './equal.predicate'; +import { Equal } from "./equal.predicate"; -describe('Equal', () => { - let v1: string; - let v2: string; - let v3: string; +describe("Equal", () => { + let v1: string; + let v2: string; + let v3: string; - let c1: Comparable; - let c2: Comparable; - let c3: Comparable; + let c1: Comparable; + let c2: Comparable; + let c3: Comparable; - beforeEach(() => { - v1 = 'Taurus'; - v2 = 'Taurus'; - v3 = 'VDK'; + beforeEach(() => { + v1 = "Taurus"; + v2 = "Taurus"; + v3 = "VDK"; - c1 = ComparableImpl.of(v1); - c2 = ComparableImpl.of(v2); - c3 = ComparableImpl.of(v3); - }); + c1 = ComparableImpl.of(v1); + c2 = ComparableImpl.of(v2); + c3 = ComparableImpl.of(v3); + }); - it('should verify instance is created', () => { - // When - const p = new Equal(c1); + it("should verify instance is created", () => { + // When + const p = new Equal(c1); - // Then - expect(p).toBeDefined(); - }); + // Then + expect(p).toBeDefined(); + }); - it('should verify value (Comparable) is correctly assigned', () => { - // When - const p = Equal.of(c1); + it("should verify value (Comparable) is correctly assigned", () => { + // When + const p = Equal.of(c1); - // Then - expect(p.comparable).toBe(c1); - }); + // Then + expect(p.comparable).toBe(c1); + }); - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const p = Equal.of(c1); - - // Then - expect(p).toBeDefined(); - expect(p).toBeInstanceOf(Equal); - }); - }); + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const p = Equal.of(c1); + + // Then + expect(p).toBeDefined(); + expect(p).toBeInstanceOf(Equal); }); + }); }); + }); - describe('Methods::()', () => { - describe('|evaluate|', () => { - it('should verify will return true if both comparables have same values, otherwise false', () => { - // Given - const p = Equal.of(c1); - - // When - const r1 = p.evaluate(c2); - const r2 = p.evaluate(c3); - const r3 = p.evaluate(c1); - - // Then - expect(r1).toBeTrue(); - expect(r2).toBeFalse(); - expect(r3).toBeTrue(); - }); - }); + describe("Methods::()", () => { + describe("|evaluate|", () => { + it("should verify will return true if both comparables have same values, otherwise false", () => { + // Given + const p = Equal.of(c1); + + // When + const r1 = p.evaluate(c2); + const r2 = p.evaluate(c3); + const r3 = p.evaluate(c1); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts index 7783a64f06..5cc9037375 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts @@ -3,34 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable } from '../../interfaces'; +import { Comparable } from "../../interfaces"; -import { SimplePredicate } from './base-simple.predicate'; +import { SimplePredicate } from "./base-simple.predicate"; /** * ** Equal Predicate that accepts Comparable and make equality evaluation. * * */ -export class Equal extends SimplePredicate { - /** - * ** Constructor. - */ - constructor(comparable: T) { - super(comparable); - } +export class Equal< + T extends Comparable = Comparable, +> extends SimplePredicate { + /** + * ** Constructor. + */ + constructor(comparable: T) { + super(comparable); + } - /** - * ** Factory method. - */ - static override of(comparable: Comparable): Equal { - return new Equal(comparable); - } + /** + * ** Factory method. + */ + static override of(comparable: Comparable): Equal { + return new Equal(comparable); + } - /** - * @inheritDoc - */ - evaluate(comparable: Comparable): boolean { - return this.comparable.equal(comparable); - } + /** + * @inheritDoc + */ + evaluate(comparable: Comparable): boolean { + return this.comparable.equal(comparable); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts index a596dbe5f9..a11e4bd7fe 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './base-simple.predicate'; -export * from './equal.predicate'; +export * from "./base-simple.predicate"; +export * from "./equal.predicate"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts index 73f6d16385..9f9af6272a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './criteria/public-api'; -export * from './error/public-api'; -export * from './http/public-api'; -export * from './interfaces/public-api'; -export * from './object/public-api'; -export * from './predicate/public-api'; -export * from './route/public-api'; -export * from './service/public-api'; -export * from './tasks/public-api'; +export * from "./criteria/public-api"; +export * from "./error/public-api"; +export * from "./http/public-api"; +export * from "./interfaces/public-api"; +export * from "./object/public-api"; +export * from "./predicate/public-api"; +export * from "./route/public-api"; +export * from "./service/public-api"; +export * from "./tasks/public-api"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts index bc50e0cd73..385f83cbca 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './route.model'; +export * from "./route.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts index ff1f1d6faa..c15c798f97 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts @@ -5,111 +5,116 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Data, NavigationExtras, Params, Route } from '@angular/router'; +import { Data, NavigationExtras, Params, Route } from "@angular/router"; -import { Replacer } from '../interfaces'; +import { Replacer } from "../interfaces"; /** * ** Interface for Navigate back command. */ export interface TaurusNavigateAction { - /** - * ** Path / Path commands where user should be navigated, returned or redirected. - * - * - If path is '$.current' - current loaded path will be use. - * - If path is '$.requested' - requested path will be use. - * - If path is '$.parent' - parent of the current path will be use. - * - Any other string - use at is and assume it as path for navigation (URL path) - * - */ - path: string | '$.current' | '$.requested' | '$.parent'; + /** + * ** Path / Path commands where user should be navigated, returned or redirected. + * + * - If path is '$.current' - current loaded path will be use. + * - If path is '$.requested' - requested path will be use. + * - If path is '$.parent' - parent of the current path will be use. + * - Any other string - use at is and assume it as path for navigation (URL path) + * + */ + path: string | "$.current" | "$.requested" | "$.parent"; - /** - * ** Replacers to tune and finalize navigate back Path / Path commands. - */ - replacers?: Array>; + /** + * ** Replacers to tune and finalize navigate back Path / Path commands. + */ + replacers?: Array>; - /** - * ** Optional query params for navigation action. - */ - queryParams?: Params; + /** + * ** Optional query params for navigation action. + */ + queryParams?: Params; - /** - * ** Optional instruction for queryParams handling. - * - * - If not provided will fallback to default one 'merge'. - * - If provided and its value is null, it won't do any handling for queryParams. - */ - queryParamsHandling?: NavigationExtras['queryParamsHandling']; + /** + * ** Optional instruction for queryParams handling. + * + * - If not provided will fallback to default one 'merge'. + * - If provided and its value is null, it won't do any handling for queryParams. + */ + queryParamsHandling?: NavigationExtras["queryParamsHandling"]; - /** - * ** Optional instruction whether to use resolved path or config path. - * - * - TRUE - will use config path, which means use it as it's configured in the routing modules. - * - FALSE - will use resolved path to the current point. - */ - useConfigPath?: boolean; + /** + * ** Optional instruction whether to use resolved path or config path. + * + * - TRUE - will use config path, which means use it as it's configured in the routing modules. + * - FALSE - will use resolved path to the current point. + */ + useConfigPath?: boolean; } /** * ** Taurus Route data with navigate to command. */ export interface TaurusRouteNavigateToData { - /** - * ** Field that has Navigate to command. - */ - navigateTo?: TaurusNavigateAction; + /** + * ** Field that has Navigate to command. + */ + navigateTo?: TaurusNavigateAction; } /** * ** Taurus Route data with navigate back command. */ export interface TaurusRouteNavigateBackData { - /** - * ** Field that has Navigate back command. - */ - navigateBack?: TaurusNavigateAction; + /** + * ** Field that has Navigate back command. + */ + navigateBack?: TaurusNavigateAction; } /** * ** Taurus Route data with redirect command. */ export interface TaurusRouteRedirectData { - /** - * ** Field that has Redirect command. - */ - redirect?: TaurusNavigateAction; + /** + * ** Field that has Redirect command. + */ + redirect?: TaurusNavigateAction; } /** * ** Custom type for Route Data with Generics. */ -export type TaurusRouteData = Record> = T & - TaurusRouteNavigateToData & - TaurusRouteNavigateBackData & - TaurusRouteRedirectData & - Data; +export type TaurusRouteData< + T extends Record = Record, +> = T & + TaurusRouteNavigateToData & + TaurusRouteNavigateBackData & + TaurusRouteRedirectData & + Data; /** * ** Taurus Route config. */ -export interface TaurusRoute extends Route { - /** - * @inheritDoc - * - * ** Static data configured per Route. - */ - data?: T; +export interface TaurusRoute< + T extends TaurusRouteData = TaurusRouteData, +> extends Route { + /** + * @inheritDoc + * + * ** Static data configured per Route. + */ + data?: T; - /** - * @inheritDoc - * - * ** Children Route configs object. - */ - children?: TaurusRoutes; + /** + * @inheritDoc + * + * ** Children Route configs object. + */ + children?: TaurusRoutes; } /** * ** Taurus Routes configs. */ -export type TaurusRoutes = TaurusRoute[]; +export type TaurusRoutes = + TaurusRoute[]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/index.ts index 6b7b2bd780..c32b85c4bc 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './model'; +export * from "./model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/index.ts index 603dc1d4aa..2f25019c1d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './taurus-base-api-service.model'; +export * from "./taurus-base-api-service.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.spec.ts index 92cd536383..c9a73d87f7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.spec.ts @@ -3,211 +3,239 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable, OnDestroy, Type } from '@angular/core'; -import { HttpStatusCode } from '@angular/common/http'; -import { TestBed } from '@angular/core/testing'; +import { Injectable, OnDestroy, Type } from "@angular/core"; +import { HttpStatusCode } from "@angular/common/http"; +import { TestBed } from "@angular/core/testing"; -import { Subscription } from 'rxjs'; +import { Subscription } from "rxjs"; -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { TaurusObject } from '../../object'; +import { TaurusObject } from "../../object"; -import { generateErrorCode, ServiceHttpErrorCodes } from '../../error'; +import { generateErrorCode, ServiceHttpErrorCodes } from "../../error"; -import { ErrorCodes, TaurusBaseApiService } from './taurus-base-api-service.model'; +import { + ErrorCodes, + TaurusBaseApiService, +} from "./taurus-base-api-service.model"; @Injectable() -class DummyApiService extends TaurusBaseApiService implements OnDestroy { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DummyApiService'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Dummy-Api-Service'; - - constructor() { - super(DummyApiService.CLASS_NAME); - - this.registerErrorCodes(DummyApiService); - } - - override dispose(): void { - super.dispose(); - } - - override ngOnDestroy(): void { - super.ngOnDestroy(); - } - - override cleanSubscriptions(): void { - super.cleanSubscriptions(); - } - - override removeSubscriptionRef(subscriptionRef: Subscription): boolean { - return super.removeSubscriptionRef(subscriptionRef); - } - - override registerErrorCodes(service: Type): void { - super.registerErrorCodes(service); - } - - loadData(): void { - this._doLoad(); - } - - modifyAssets(): void { - this.doModify(); - } - - protected doModify(): void { - // No-ops. - } - - private _doLoad(): void { - // No-ops. - } +class DummyApiService + extends TaurusBaseApiService + implements OnDestroy +{ + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DummyApiService"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Dummy-Api-Service"; + + constructor() { + super(DummyApiService.CLASS_NAME); + + this.registerErrorCodes(DummyApiService); + } + + override dispose(): void { + super.dispose(); + } + + override ngOnDestroy(): void { + super.ngOnDestroy(); + } + + override cleanSubscriptions(): void { + super.cleanSubscriptions(); + } + + override removeSubscriptionRef(subscriptionRef: Subscription): boolean { + return super.removeSubscriptionRef(subscriptionRef); + } + + override registerErrorCodes(service: Type): void { + super.registerErrorCodes(service); + } + + loadData(): void { + this._doLoad(); + } + + modifyAssets(): void { + this.doModify(); + } + + protected doModify(): void { + // No-ops. + } + + private _doLoad(): void { + // No-ops. + } } -describe('TaurusBaseApiService', () => { - let service: DummyApiService; - let spyForRegisterErrorCodes: jasmine.Spy; +describe("TaurusBaseApiService", () => { + let service: DummyApiService; + let spyForRegisterErrorCodes: jasmine.Spy; - beforeEach(() => { - spyForRegisterErrorCodes = spyOn(DummyApiService.prototype, 'registerErrorCodes').and.callThrough(); + beforeEach(() => { + spyForRegisterErrorCodes = spyOn( + DummyApiService.prototype, + "registerErrorCodes", + ).and.callThrough(); - TestBed.configureTestingModule({ - providers: [{ provide: DummyApiService, useClass: DummyApiService }] + TestBed.configureTestingModule({ + providers: [{ provide: DummyApiService, useClass: DummyApiService }], + }); + + service = TestBed.inject(DummyApiService); + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + expect(service).toBeInstanceOf(DummyApiService); + expect(service).toBeInstanceOf(TaurusBaseApiService); + expect(service).toBeInstanceOf(TaurusObject); + }); + + describe("Statics::", () => { + describe("Properties::", () => { + describe("|CLASS_NAME|", () => { + it("should verify the value", () => { + // Then + expect(DummyApiService.CLASS_NAME).toEqual("DummyApiService"); }); + }); - service = TestBed.inject(DummyApiService); + describe("|PUBLIC_NAME|", () => { + it("should verify the value", () => { + // Then + expect(DummyApiService.PUBLIC_NAME).toEqual("Dummy-Api-Service"); + }); + }); }); + }); - it('should verify instance is created', () => { + describe("Properties::", () => { + describe("|errorCodes|", () => { + it("should verify value has keys only for public methods (methods that not start with underscore) and names are not in blacklist", () => { // Then - expect(service).toBeDefined(); - expect(service).toBeInstanceOf(DummyApiService); - expect(service).toBeInstanceOf(TaurusBaseApiService); - expect(service).toBeInstanceOf(TaurusObject); - }); + expect(service.errorCodes).toBeDefined(); + expect(service.errorCodes).toBeInstanceOf(Object); + }); - describe('Statics::', () => { - describe('Properties::', () => { - describe('|CLASS_NAME|', () => { - it('should verify the value', () => { - // Then - expect(DummyApiService.CLASS_NAME).toEqual('DummyApiService'); - }); - }); - - describe('|PUBLIC_NAME|', () => { - it('should verify the value', () => { - // Then - expect(DummyApiService.PUBLIC_NAME).toEqual('Dummy-Api-Service'); - }); - }); - }); + it("should verify expected keys are present", () => { + // Then + const keys = Object.keys(service.errorCodes); + expect(keys.length).toEqual(3); + expect(keys.findIndex((k) => k === "doModify")).toBeGreaterThan(-1); + expect(keys.findIndex((k) => k === "modifyAssets")).toBeGreaterThan(-1); + expect(keys.findIndex((k) => k === "loadData")).toBeGreaterThan(-1); + }); + + it("should verify values for every public key as provided by intellisense", () => { + // Then + const methods: Array> = [ + "loadData", + "modifyAssets", + ]; + const additionalDetails: Array<[keyof ServiceHttpErrorCodes, string]> = + [ + ["All", null], + ["BadRequest", `${HttpStatusCode.BadRequest}`], + ["Unauthorized", `${HttpStatusCode.Unauthorized}`], + ["Forbidden", `${HttpStatusCode.Forbidden}`], + ["NotFound", `${HttpStatusCode.NotFound}`], + ["MethodNotAllowed", `${HttpStatusCode.MethodNotAllowed}`], + ["Conflict", `${HttpStatusCode.Conflict}`], + ["UnprocessableEntity", `${HttpStatusCode.UnprocessableEntity}`], + ["InternalServerError", `${HttpStatusCode.InternalServerError}`], + ["ServiceUnavailable", `${HttpStatusCode.ServiceUnavailable}`], + ["Unknown", "unknown"], + ]; + + for (const method of methods) { + const numberOfAvailableErrorCodesPerMethod = Object.keys( + service.errorCodes[method], + ).length; + + // verify number of available error codes per method is 11 + expect(numberOfAvailableErrorCodesPerMethod).toEqual(13); + + for (const [key, code] of additionalDetails) { + // verify every error code is of the expected pattern + expect(service.errorCodes[method][key]).toEqual( + generateErrorCode( + DummyApiService.CLASS_NAME, + DummyApiService.PUBLIC_NAME, + method, + code, + ), + ); + } + } + }); }); - describe('Properties::', () => { - describe('|errorCodes|', () => { - it('should verify value has keys only for public methods (methods that not start with underscore) and names are not in blacklist', () => { - // Then - expect(service.errorCodes).toBeDefined(); - expect(service.errorCodes).toBeInstanceOf(Object); - }); - - it('should verify expected keys are present', () => { - // Then - const keys = Object.keys(service.errorCodes); - expect(keys.length).toEqual(3); - expect(keys.findIndex((k) => k === 'doModify')).toBeGreaterThan(-1); - expect(keys.findIndex((k) => k === 'modifyAssets')).toBeGreaterThan(-1); - expect(keys.findIndex((k) => k === 'loadData')).toBeGreaterThan(-1); - }); - - it('should verify values for every public key as provided by intellisense', () => { - // Then - const methods: Array> = ['loadData', 'modifyAssets']; - const additionalDetails: Array<[keyof ServiceHttpErrorCodes, string]> = [ - ['All', null], - ['BadRequest', `${HttpStatusCode.BadRequest}`], - ['Unauthorized', `${HttpStatusCode.Unauthorized}`], - ['Forbidden', `${HttpStatusCode.Forbidden}`], - ['NotFound', `${HttpStatusCode.NotFound}`], - ['MethodNotAllowed', `${HttpStatusCode.MethodNotAllowed}`], - ['Conflict', `${HttpStatusCode.Conflict}`], - ['UnprocessableEntity', `${HttpStatusCode.UnprocessableEntity}`], - ['InternalServerError', `${HttpStatusCode.InternalServerError}`], - ['ServiceUnavailable', `${HttpStatusCode.ServiceUnavailable}`], - ['Unknown', 'unknown'] - ]; - - for (const method of methods) { - const numberOfAvailableErrorCodesPerMethod = Object.keys(service.errorCodes[method]).length; - - // verify number of available error codes per method is 11 - expect(numberOfAvailableErrorCodesPerMethod).toEqual(13); - - for (const [key, code] of additionalDetails) { - // verify every error code is of the expected pattern - expect(service.errorCodes[method][key]).toEqual( - generateErrorCode(DummyApiService.CLASS_NAME, DummyApiService.PUBLIC_NAME, method, code) - ); - } - } - }); - }); + describe("|objectUUID|", () => { + it("should verify value is DummyApiService", () => { + // Given + spyOn(CollectionsUtil, "generateObjectUUID").and.callFake( + (value) => value, + ); - describe('|objectUUID|', () => { - it('should verify value is DummyApiService', () => { - // Given - spyOn(CollectionsUtil, 'generateObjectUUID').and.callFake((value) => value); + // When + service = new DummyApiService(); - // When - service = new DummyApiService(); - - // Then - expect(service.objectUUID).toEqual('DummyApiService'); - }); + // Then + expect(service.objectUUID).toEqual("DummyApiService"); + }); - it('should verify value is TaurusBaseApiService', () => { - // Given - spyOn(CollectionsUtil, 'generateObjectUUID').and.callFake((value) => value); + it("should verify value is TaurusBaseApiService", () => { + // Given + spyOn(CollectionsUtil, "generateObjectUUID").and.callFake( + (value) => value, + ); - // When - // @ts-ignore - service = new TaurusBaseApiService(); + // When + // @ts-ignore + service = new TaurusBaseApiService(); - // Then - expect(service.objectUUID).toEqual('TaurusBaseApiService'); - }); - }); + // Then + expect(service.objectUUID).toEqual("TaurusBaseApiService"); + }); }); + }); - describe('Methods::', () => { - describe('|registerErrorCodes|', () => { - it('should verify method is executed during the class initialization', () => { - // Then - expect(spyForRegisterErrorCodes).toHaveBeenCalled(); - }); + describe("Methods::", () => { + describe("|registerErrorCodes|", () => { + it("should verify method is executed during the class initialization", () => { + // Then + expect(spyForRegisterErrorCodes).toHaveBeenCalled(); + }); - it('should verify thrown error will log in console', () => { - // Given - spyOn(CollectionsUtil, 'isFunction').and.throwError(new Error('Random Error')); - const consoleSpy = spyOn(console, 'error').and.callFake(CallFake); + it("should verify thrown error will log in console", () => { + // Given + spyOn(CollectionsUtil, "isFunction").and.throwError( + new Error("Random Error"), + ); + const consoleSpy = spyOn(console, "error").and.callFake(CallFake); - // When - service = new DummyApiService(); + // When + service = new DummyApiService(); - // Then - expect(consoleSpy).toHaveBeenCalledWith('Cannot register Service Error Codes!'); - }); - }); + // Then + expect(consoleSpy).toHaveBeenCalledWith( + "Cannot register Service Error Codes!", + ); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.ts index 89e4f19dcd..9d4dd3ccdf 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/model/taurus-base-api-service.model.ts @@ -5,33 +5,39 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/naming-convention */ -import { Directive, Type } from '@angular/core'; +import { Directive, Type } from "@angular/core"; -import { CollectionsUtil, FilterMethods } from '../../../utils'; +import { CollectionsUtil, FilterMethods } from "../../../utils"; -import { TaurusObject } from '../../object'; +import { TaurusObject } from "../../object"; -import { ServiceHttpErrorCodes } from '../../error'; -import { generateSupportedHttpErrorCodes } from '../../error/utils'; +import { ServiceHttpErrorCodes } from "../../error"; +import { generateSupportedHttpErrorCodes } from "../../error/utils"; /** * ** Store type for auto-generated codes for every method, where key is the method name and value is different error codes for auto-supported scenarios. */ -export type ErrorCodes = Readonly< - Record, Readonly>> +export type ErrorCodes< + CType, + KExclude extends keyof any = ExcludedMethods, +> = Readonly< + Record< + keyof FilterMethods, + Readonly> + > >; /** * ** Excluded methods from auto-generated error codes. */ export type ExcludedMethods = - | 'constructor' - | 'errorCodes' - | 'dispose' - | 'ngOnDestroy' - | 'cleanSubscriptions' - | 'removeSubscriptionRef' - | 'registerErrorCodes'; + | "constructor" + | "errorCodes" + | "dispose" + | "ngOnDestroy" + | "cleanSubscriptions" + | "removeSubscriptionRef" + | "registerErrorCodes"; /** * ** Base Class for Angular Service related classes. @@ -40,69 +46,71 @@ export type ExcludedMethods = * - There could be added additional error codes from subclasses. */ @Directive() -export class TaurusBaseApiService extends TaurusObject { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'TaurusBaseApiService'; +export class TaurusBaseApiService< + CType extends TaurusBaseApiService = any, +> extends TaurusObject { + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "TaurusBaseApiService"; - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Taurus-Api-Service'; + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Taurus-Api-Service"; - /** - * ** Class error codes Mapping. - * - * - There are auto-generated error codes for every method name in runtime, where method name is the key, and the multiple values where each value is bound to unique error code. - * - Subclasses could override in definition time or in runtime to add additional error codes. - */ - readonly errorCodes: ErrorCodes = {} as ErrorCodes; + /** + * ** Class error codes Mapping. + * + * - There are auto-generated error codes for every method name in runtime, where method name is the key, and the multiple values where each value is bound to unique error code. + * - Subclasses could override in definition time or in runtime to add additional error codes. + */ + readonly errorCodes: ErrorCodes = {} as ErrorCodes; - /** - * ** Constructor. - */ - protected constructor(className: string = null) { - super(className ?? TaurusBaseApiService.CLASS_NAME); - } + /** + * ** Constructor. + */ + protected constructor(className: string = null) { + super(className ?? TaurusBaseApiService.CLASS_NAME); + } - /** - * ** Register error codes for service. - * - * - Exclude system methods. - * - Exclude private methods which names start with underscore (e.g. _methodName(): void;) - * @protected - */ - protected registerErrorCodes(service: Type): void { - /* eslint-disable @typescript-eslint/no-unsafe-argument, + /** + * ** Register error codes for service. + * + * - Exclude system methods. + * - Exclude private methods which names start with underscore (e.g. _methodName(): void;) + * @protected + */ + protected registerErrorCodes(service: Type): void { + /* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ - try { - Object.getOwnPropertyNames(service.prototype) - .filter( - (method) => - method !== 'constructor' && - method !== 'dispose' && - method !== 'ngOnDestroy' && - method !== 'cleanSubscriptions' && - method !== 'removeSubscriptionRef' && - method !== 'registerErrorCodes' && - !/^_/.test(method) && - CollectionsUtil.isFunction(service.prototype[method]) - ) - .forEach((method) => { - this.errorCodes[method] = generateSupportedHttpErrorCodes( - (service as any).CLASS_NAME, - (service as any).PUBLIC_NAME, - method - ); - }); - } catch (e) { - console.error(`Cannot register Service Error Codes!`); - } - /* eslint-enable @typescript-eslint/no-unsafe-argument, + try { + Object.getOwnPropertyNames(service.prototype) + .filter( + (method) => + method !== "constructor" && + method !== "dispose" && + method !== "ngOnDestroy" && + method !== "cleanSubscriptions" && + method !== "removeSubscriptionRef" && + method !== "registerErrorCodes" && + !/^_/.test(method) && + CollectionsUtil.isFunction(service.prototype[method]), + ) + .forEach((method) => { + this.errorCodes[method] = generateSupportedHttpErrorCodes( + (service as any).CLASS_NAME, + (service as any).PUBLIC_NAME, + method, + ); + }); + } catch (e) { + console.error(`Cannot register Service Error Codes!`); + } + /* eslint-enable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ - } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/service/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts index c692b2468f..4c676bee62 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './task.model'; +export * from "./task.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts index 4f0ab16bd4..c8f10b7c30 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts @@ -3,52 +3,61 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../utils'; - -import { createTaskIdentifier, extractTaskFromIdentifier } from './task.model'; - -describe('Tasks', () => { - describe('|createTaskIdentifier|', () => { - it('should verify will return expected value', () => { - // Given - const task = 'create_entity'; - const dateNowISO = new Date().toISOString(); - const taskIdentifier = `${task} __ ${dateNowISO}`; - const dateISOSpy = spyOn(CollectionsUtil, 'dateISO').and.returnValue(dateNowISO); - const interpolateStringSpy = spyOn(CollectionsUtil, 'interpolateString' as never).and.returnValue(taskIdentifier as never); - const expected = `${task} __ ${dateNowISO}`; - - // When - const response = createTaskIdentifier(task); - - // Then - expect(response).toEqual(expected); - expect(dateISOSpy).toHaveBeenCalled(); - // @ts-ignore - expect(interpolateStringSpy).toHaveBeenCalledWith('%s __ %s' as never, task as never, dateNowISO as never); - }); - - it('should verify will return value undefined when no task provided', () => { - // When - const response = createTaskIdentifier(null); - - // Then - expect(response).toBeUndefined(); - }); +import { CollectionsUtil } from "../../utils"; + +import { createTaskIdentifier, extractTaskFromIdentifier } from "./task.model"; + +describe("Tasks", () => { + describe("|createTaskIdentifier|", () => { + it("should verify will return expected value", () => { + // Given + const task = "create_entity"; + const dateNowISO = new Date().toISOString(); + const taskIdentifier = `${task} __ ${dateNowISO}`; + const dateISOSpy = spyOn(CollectionsUtil, "dateISO").and.returnValue( + dateNowISO, + ); + const interpolateStringSpy = spyOn( + CollectionsUtil, + "interpolateString" as never, + ).and.returnValue(taskIdentifier as never); + const expected = `${task} __ ${dateNowISO}`; + + // When + const response = createTaskIdentifier(task); + + // Then + expect(response).toEqual(expected); + expect(dateISOSpy).toHaveBeenCalled(); + // @ts-ignore + expect(interpolateStringSpy).toHaveBeenCalledWith( + "%s __ %s" as never, + task as never, + dateNowISO as never, + ); }); - describe('|extractTaskFromIdentifier|', () => { - it('should verify will return Task from provided identifier', () => { - // Given - const dateNowISO = new Date().toISOString(); - const taskIdentifier = `delete_entity __ ${dateNowISO}`; - const expected = 'delete_entity'; + it("should verify will return value undefined when no task provided", () => { + // When + const response = createTaskIdentifier(null); - // When - const response = extractTaskFromIdentifier(taskIdentifier); + // Then + expect(response).toBeUndefined(); + }); + }); + + describe("|extractTaskFromIdentifier|", () => { + it("should verify will return Task from provided identifier", () => { + // Given + const dateNowISO = new Date().toISOString(); + const taskIdentifier = `delete_entity __ ${dateNowISO}`; + const expected = "delete_entity"; + + // When + const response = extractTaskFromIdentifier(taskIdentifier); - // Then - expect(response).toEqual(expected); - }); + // Then + expect(response).toEqual(expected); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts index 7a146b246d..0db02c550d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts @@ -3,29 +3,35 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../utils'; +import { CollectionsUtil } from "../../utils"; -const TASK_IDENTIFIER_SEPARATOR = ' __ '; +const TASK_IDENTIFIER_SEPARATOR = " __ "; const TASK_IDENTIFIER_TEMPLATE = `%s${TASK_IDENTIFIER_SEPARATOR}%s`; /** * ** Factory for Tasks identifiers. */ export const createTaskIdentifier = (task: string) => { - if (CollectionsUtil.isString(task)) { - return CollectionsUtil.interpolateString(TASK_IDENTIFIER_TEMPLATE, task, CollectionsUtil.dateISO()); - } + if (CollectionsUtil.isString(task)) { + return CollectionsUtil.interpolateString( + TASK_IDENTIFIER_TEMPLATE, + task, + CollectionsUtil.dateISO(), + ); + } - return undefined; + return undefined; }; /** * ** Extract Task from Tasks identifiers. */ -export const extractTaskFromIdentifier = (taskIdentifier: string) => { - if (CollectionsUtil.isString(taskIdentifier)) { - return taskIdentifier.split(TASK_IDENTIFIER_SEPARATOR)[0] as T; - } +export const extractTaskFromIdentifier = ( + taskIdentifier: string, +) => { + if (CollectionsUtil.isString(taskIdentifier)) { + return taskIdentifier.split(TASK_IDENTIFIER_SEPARATOR)[0] as T; + } - return null; + return null; }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss index 7d689db2cf..d69f0a7782 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss @@ -25,25 +25,25 @@ $xxl-min-width: 1919px; $min-el-size: 310px; .medium-and-up { - @media screen and (max-width: $medium-width2) { - display: none !important; - } + @media screen and (max-width: $medium-width2) { + display: none !important; + } } .small-and-up { - @media screen and (max-width: $xs-max) { - display: none !important; - } + @media screen and (max-width: $xs-max) { + display: none !important; + } } .medium-and-down { - @media screen and (min-width: $medium-width) { - display: none !important; - } + @media screen and (min-width: $medium-width) { + display: none !important; + } } .small-and-down { - @media screen and (min-width: $small-width) { - display: none !important; - } + @media screen and (min-width: $small-width) { + display: none !important; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss index 6a52d334f4..8b0ca10c12 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss @@ -25,14 +25,14 @@ * ``` */ @mixin light-theme { - :host-context(body:not([cds-theme])) &, - :host-context(body[cds-theme='light']) & { - @content; - } + :host-context(body:not([cds-theme])) &, + :host-context(body[cds-theme="light"]) & { + @content; + } } @mixin dark-theme { - :host-context(body[cds-theme='dark']) & { - @content; - } + :host-context(body[cds-theme="dark"]) & { + @content; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts index 3ce97cd489..e261234aed 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './ngx-components'; -export * from './ngx-utils'; +export * from "./ngx-components"; +export * from "./ngx-utils"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts index e54578ed0b..871119054d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts @@ -6,64 +6,64 @@ // –——— CLARITY ANIMATIONS ————— // ATOMIC Animations // primary -export const atomicPrimaryEnterCurve = 'cubic-bezier(0, 1.5, 0.5, 1)'; +export const atomicPrimaryEnterCurve = "cubic-bezier(0, 1.5, 0.5, 1)"; export const atomicPrimaryEnterTiming = 200; -export const atomicPrimaryLeaveCurve = 'cubic-bezier(0,.99,0,.99)'; +export const atomicPrimaryLeaveCurve = "cubic-bezier(0,.99,0,.99)"; export const atomicPrimaryLeaveTiming = 200; // secondary -export const atomicSecondaryEnterCurve = 'cubic-bezier(0, 1.5, 0.5, 1)'; +export const atomicSecondaryEnterCurve = "cubic-bezier(0, 1.5, 0.5, 1)"; export const atomicSecondaryEnterTiming = 400; -export const atomicSecondaryLeaveCurve = 'cubic-bezier(0, 1.5, 0.5, 1)'; +export const atomicSecondaryLeaveCurve = "cubic-bezier(0, 1.5, 0.5, 1)"; export const atomicSecondaryLeaveTiming = 100; // COMPONENT Animations // primary -export const componentPrimaryEnterCurve = 'cubic-bezier(0,.99,0,.99)'; +export const componentPrimaryEnterCurve = "cubic-bezier(0,.99,0,.99)"; export const componentPrimaryEnterTiming = 400; -export const componentPrimaryLeaveCurve = 'cubic-bezier(0,.99,0,.99)'; +export const componentPrimaryLeaveCurve = "cubic-bezier(0,.99,0,.99)"; export const componentPrimaryLeaveTiming = 300; // PAGE Animations // primary -export const pagePrimaryEnterCurve = 'cubic-bezier(0,.99,0,.99)'; +export const pagePrimaryEnterCurve = "cubic-bezier(0,.99,0,.99)"; export const pagePrimaryEnterTiming = 250; -export const pagePrimaryLeaveCurve = 'cubic-bezier(0,.99,0,.99)'; +export const pagePrimaryLeaveCurve = "cubic-bezier(0,.99,0,.99)"; export const pagePrimaryLeaveTiming = 200; // PROGRESS Animations // primary -export const progressPrimaryCurve = 'cubic-bezier(.17,.4,.8,.79)'; +export const progressPrimaryCurve = "cubic-bezier(.17,.4,.8,.79)"; export const progressPrimaryTiming = 790; // secondary -export const progressSecondaryCurve = 'cubic-bezier(.34,.01,.39,1)'; +export const progressSecondaryCurve = "cubic-bezier(.34,.01,.39,1)"; export const progressSecondaryTiming = 200; // ICON Animations // primary -export const linePrimaryEnterCurve = 'linear'; +export const linePrimaryEnterCurve = "linear"; export const linePrimaryEnterTiming = 250; export const linePrimaryEnterDelay = 200; // secondary -export const lineSecondaryEnterCurve = 'linear'; +export const lineSecondaryEnterCurve = "linear"; export const lineSecondaryEnterTiming = 400; export const lineSecondaryEnterDelay = 200; // –——— NGX ONLY ANIMATIONS ————— export const DISMISS_ICON_DURATION = 300; export const DISMISS_ICON_DELAY = 350; -export const DISMISS_ICON_CURVE = 'cubic-bezier(0, 1.2, 0.7, 1)'; +export const DISMISS_ICON_CURVE = "cubic-bezier(0, 1.2, 0.7, 1)"; export const GRADIENT_DURATION = 500; export const GRADIENT_DELAY = 100; -export const GRADIENT_LEAVE_CURVE = 'cubic-bezier(0, 1.2, 0.7, 1)'; +export const GRADIENT_LEAVE_CURVE = "cubic-bezier(0, 1.2, 0.7, 1)"; export const STAGGER_DURATION = 200; // used for animation debugging const ANIMATION_MULTIPLIER = 1; export function multiply(value: number) { - return value * ANIMATION_MULTIPLIER; + return value * ANIMATION_MULTIPLIER; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html index 7e586c84c3..2d0b640794 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html @@ -3,56 +3,63 @@ ~ SPDX-License-Identifier: Apache-2.0 --> - - - + + + - - {{tooltip}} - - + + {{ tooltip }} + + - {{copyAlert}} + {{ copyAlert }} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss index e632203d1a..012e66e331 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss @@ -4,59 +4,63 @@ */ span.copy-button { - button { - margin-right: 0px !important; - border: none; - padding: 0; - } + button { + margin-right: 0px !important; + border: none; + padding: 0; + } } .flip-horizontal-bottom { - -webkit-animation: flip-horizontal-bottom 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; - animation: flip-horizontal-bottom 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; + -webkit-animation: flip-horizontal-bottom 0.5s + cubic-bezier(0.455, 0.03, 0.515, 0.955) both; + animation: flip-horizontal-bottom 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) + both; } .flip-horizontal-reverse { - -webkit-animation: flip-horizontal-reverse 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; - animation: flip-horizontal-reverse 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) both; + -webkit-animation: flip-horizontal-reverse 0.5s + cubic-bezier(0.455, 0.03, 0.515, 0.955) both; + animation: flip-horizontal-reverse 0.5s + cubic-bezier(0.455, 0.03, 0.515, 0.955) both; } .anim-object { - transform-style: preserve-3d; - display: block; - outline: none; + transform-style: preserve-3d; + display: block; + outline: none; } .face { - position: absolute; - width: 100%; - height: 100%; - backface-visibility: hidden; - border-radius: 3px; - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - transform: rotateX(0deg); + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: 3px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + transform: rotateX(0deg); } .front { - z-index: 20; + z-index: 20; - &.face-label { - position: relative; - } + &.face-label { + position: relative; + } } .back { - z-index: 10; - transform: rotateX(180deg); - &.face-label { - top: 0px; - } + z-index: 10; + transform: rotateX(180deg); + &.face-label { + top: 0px; + } } .hide-tooltip { - display: none; + display: none; } /** @@ -65,45 +69,45 @@ span.copy-button { * ---------------------------------------- */ @-webkit-keyframes flip-horizontal-bottom { - 0% { - -webkit-transform: rotateX(0); - transform: rotateX(0); - } - 100% { - -webkit-transform: rotateX(-180deg); - transform: rotateX(-180deg); - } + 0% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } + 100% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } } @keyframes flip-horizontal-bottom { - 0% { - -webkit-transform: rotateX(0); - transform: rotateX(0); - } - 100% { - -webkit-transform: rotateX(-180deg); - transform: rotateX(-180deg); - } + 0% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } + 100% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } } @-webkit-keyframes flip-horizontal-reverse { - 0% { - -webkit-transform: rotateX(-180deg); - transform: rotateX(-180deg); - } - 100% { - -webkit-transform: rotateX(0); - transform: rotateX(0); - } + 0% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } + 100% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } } @keyframes flip-horizontal-reverse { - 0% { - -webkit-transform: rotateX(-180deg); - transform: rotateX(-180deg); - } - 100% { - -webkit-transform: rotateX(0); - transform: rotateX(0); - } + 0% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } + 100% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts index 1d7f816122..48017d046b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts @@ -3,87 +3,95 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ViewChild, Component } from '@angular/core'; -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ViewChild, Component } from "@angular/core"; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from "@angular/core/testing"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { VdkCopyToClipboardButtonComponent } from './copy-to-clipboard-button.component'; +import { VdkCopyToClipboardButtonComponent } from "./copy-to-clipboard-button.component"; -describe('CopyToClipboardButtonComponent', () => { - let component: VdkCopyToClipboardButtonComponent; - let hostComponent: TestHostComponent; - let fixture: ComponentFixture; +describe("CopyToClipboardButtonComponent", () => { + let component: VdkCopyToClipboardButtonComponent; + let hostComponent: TestHostComponent; + let fixture: ComponentFixture; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ClarityModule], - declarations: [TestHostComponent, VdkCopyToClipboardButtonComponent] - }); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ClarityModule], + declarations: [TestHostComponent, VdkCopyToClipboardButtonComponent], }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostComponent); + hostComponent = fixture.componentInstance; + component = hostComponent.component; + }); + describe("when no btnLabel specified", () => { beforeEach(() => { - fixture = TestBed.createComponent(TestHostComponent); - hostComponent = fixture.componentInstance; - component = hostComponent.component; + fixture.detectChanges(); }); - describe('when no btnLabel specified', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - it('should initialize size properly', () => { - component.ngOnInit(); - expect(component.bounds).toBe('22px'); - component.size = 24; - component.ngOnInit(); - expect(component.bounds).toBe('30px'); - }); - - it('should copy the value to the clipboard', () => { - spyOn(component, 'copyToClipboard'); - component.doCopy(); - expect(component.copyToClipboard).toHaveBeenCalledWith('Test string'); - }); + it("should initialize size properly", () => { + component.ngOnInit(); + expect(component.bounds).toBe("22px"); + component.size = 24; + component.ngOnInit(); + expect(component.bounds).toBe("30px"); + }); - it('should set the proper "copied" status', fakeAsync(() => { - component.doCopy(); - expect(component.copied).toBeTruthy(); - tick(1500); - expect(component.copied).toBeFalsy(); - })); + it("should copy the value to the clipboard", () => { + spyOn(component, "copyToClipboard"); + component.doCopy(); + expect(component.copyToClipboard).toHaveBeenCalledWith("Test string"); + }); - // it('should show the copy icon', () => { - // expect(element).toHaveRendered('.copy-button'); - // }); + it('should set the proper "copied" status', fakeAsync(() => { + component.doCopy(); + expect(component.copied).toBeTruthy(); + tick(1500); + expect(component.copied).toBeFalsy(); + })); - // it('should not show the copy button with text', () => { - // expect(element).not.toHaveRendered('button.btn.btn-outline'); - // }); - }); + // it('should show the copy icon', () => { + // expect(element).toHaveRendered('.copy-button'); + // }); - // describe('when btnLabel specified', () => { - // beforeEach(() => { - // component.btnLabel = 'COPY TO CLIPBOARD'; - // fixture.detectChanges(); - // }); - // - // it('should show the copy button with text', () => { - // expect(element).toHaveRendered('button.btn.btn-outline'); - // - // let label = element.querySelector('button .face-label span'); - // expect(label.innerText.trim()).toEqual('COPY TO CLIPBOARD'); - // }); + // it('should not show the copy button with text', () => { + // expect(element).not.toHaveRendered('button.btn.btn-outline'); // }); + }); + + // describe('when btnLabel specified', () => { + // beforeEach(() => { + // component.btnLabel = 'COPY TO CLIPBOARD'; + // fixture.detectChanges(); + // }); + // + // it('should show the copy button with text', () => { + // expect(element).toHaveRendered('button.btn.btn-outline'); + // + // let label = element.querySelector('button .face-label span'); + // expect(label.innerText.trim()).toEqual('COPY TO CLIPBOARD'); + // }); + // }); }); @Component({ - template: ` ` + template: ` + + + `, }) class TestHostComponent { - @ViewChild(VdkCopyToClipboardButtonComponent, { static: true }) - component: VdkCopyToClipboardButtonComponent; + @ViewChild(VdkCopyToClipboardButtonComponent, { static: true }) + component: VdkCopyToClipboardButtonComponent; - public value = 'Test string'; + public value = "Test string"; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts index af97a20db8..174993bcb6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts @@ -5,118 +5,134 @@ /* eslint-disable */ -import { ElementRef, HostBinding, Output, EventEmitter, Component, OnInit, Input, ViewChild } from '@angular/core'; -import { VdkSimpleTranslateService } from '../../ngx-utils'; - -import { TRANSLATIONS } from './copy-to-clipboard-button.l10n'; +import { + ElementRef, + HostBinding, + Output, + EventEmitter, + Component, + OnInit, + Input, + ViewChild, +} from "@angular/core"; +import { VdkSimpleTranslateService } from "../../ngx-utils"; + +import { TRANSLATIONS } from "./copy-to-clipboard-button.l10n"; const SHOW_CHECKBOX_TIMEOUT = 1500; @Component({ - selector: 'vdk-copy-to-clipboard-button', - templateUrl: './copy-to-clipboard-button.component.html', - styleUrls: ['./copy-to-clipboard-button.component.scss'] + selector: "vdk-copy-to-clipboard-button", + templateUrl: "./copy-to-clipboard-button.component.html", + styleUrls: ["./copy-to-clipboard-button.component.scss"], }) export class VdkCopyToClipboardButtonComponent implements OnInit { - @ViewChild('area', { read: ElementRef, static: false }) area: ElementRef; - - @HostBinding('class') @Input('class') classList: string = ''; - - @Input() value: string; - @Input() ariaLabel: string = ''; - @Input() copyAlert: string; - @Input() size = 16; - @Input() tooltip = ''; - @Input() btnLabel = ''; // if no label specified, show the normal copy icon - @Input() btnClasses = ['btn-outline']; // if no label specified, show the normal copy icon - @Input() disabled: boolean = false; - @Input() tooltipDirection = 'top-left'; - - @Output() copyClick = new EventEmitter(); - - private firstLoad = true; // show correct icon first - - btnClassesToApply: string; - bounds: string; - hasProjectedContent: boolean = false; - isSafari: boolean = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - copied: boolean = false; - - constructor( - private el: ElementRef, - public translateService: VdkSimpleTranslateService - ) { - this.translateService.loadTranslationsForComponent('copy-to-clipboard-button', TRANSLATIONS); - } - - ngOnInit() { - this.bounds = this.size + 6 + 'px'; - - this.hasProjectedContent = this.el.nativeElement.innerText.trim(); - - this.calculateClassesToApply(); - this.copyAlert = this.copyAlert || this.translateService.translate('copy-to-clipboard-button.copied'); + @ViewChild("area", { read: ElementRef, static: false }) area: ElementRef; + + @HostBinding("class") @Input("class") classList: string = ""; + + @Input() value: string; + @Input() ariaLabel: string = ""; + @Input() copyAlert: string; + @Input() size = 16; + @Input() tooltip = ""; + @Input() btnLabel = ""; // if no label specified, show the normal copy icon + @Input() btnClasses = ["btn-outline"]; // if no label specified, show the normal copy icon + @Input() disabled: boolean = false; + @Input() tooltipDirection = "top-left"; + + @Output() copyClick = new EventEmitter(); + + private firstLoad = true; // show correct icon first + + btnClassesToApply: string; + bounds: string; + hasProjectedContent: boolean = false; + isSafari: boolean = /^((?!chrome|android).)*safari/i.test( + navigator.userAgent, + ); + copied: boolean = false; + + constructor( + private el: ElementRef, + public translateService: VdkSimpleTranslateService, + ) { + this.translateService.loadTranslationsForComponent( + "copy-to-clipboard-button", + TRANSLATIONS, + ); + } + + ngOnInit() { + this.bounds = this.size + 6 + "px"; + + this.hasProjectedContent = this.el.nativeElement.innerText.trim(); + + this.calculateClassesToApply(); + this.copyAlert = + this.copyAlert || + this.translateService.translate("copy-to-clipboard-button.copied"); + } + + calculateClassesToApply() { + let classes: Array = []; + + if (!this.btnLabel.length) { + classes.push("icon-btn"); } - calculateClassesToApply() { - let classes: Array = []; - - if (!this.btnLabel.length) { - classes.push('icon-btn'); - } - - if (this.btnLabel.length) { - classes = classes.concat(this.btnClasses); - } - - if (this.disabled) { - classes.push('disabled'); - } - - this.btnClassesToApply = classes.join(' ') + ' ' + this.classList; + if (this.btnLabel.length) { + classes = classes.concat(this.btnClasses); } - ngOnChanges() { - this.calculateClassesToApply(); + if (this.disabled) { + classes.push("disabled"); } - copyToClipboard(val: string) { - let myWindow: any = window; + this.btnClassesToApply = classes.join(" ") + " " + this.classList; + } - let onCopy = (e: ClipboardEvent) => { - e.preventDefault(); + ngOnChanges() { + this.calculateClassesToApply(); + } - if (e.clipboardData) { - e.clipboardData.setData('text/plain', val); - } else if (myWindow.clipboardData) { - myWindow.clipboardData.setData('Text', val); - } + copyToClipboard(val: string) { + let myWindow: any = window; - myWindow.removeEventListener('copy', onCopy); - }; + let onCopy = (e: ClipboardEvent) => { + e.preventDefault(); - if (this.isSafari) { - this.area.nativeElement.value = val; - this.area.nativeElement.select(); - navigator.clipboard?.writeText(val); - } + if (e.clipboardData) { + e.clipboardData.setData("text/plain", val); + } else if (myWindow.clipboardData) { + myWindow.clipboardData.setData("Text", val); + } - myWindow.addEventListener('copy', onCopy); + myWindow.removeEventListener("copy", onCopy); + }; - if (myWindow.clipboardData && myWindow.clipboardData.setData) { - myWindow.clipboardData.setData('Text', val); - } else { - document.execCommand('copy'); - } + if (this.isSafari) { + this.area.nativeElement.value = val; + this.area.nativeElement.select(); + navigator.clipboard?.writeText(val); } - doCopy() { - this.copyToClipboard(this.value); - this.copyClick.emit(); - this.firstLoad = false; - this.copied = true; - setTimeout(() => { - this.copied = false; - }, SHOW_CHECKBOX_TIMEOUT); + myWindow.addEventListener("copy", onCopy); + + if (myWindow.clipboardData && myWindow.clipboardData.setData) { + myWindow.clipboardData.setData("Text", val); + } else { + document.execCommand("copy"); } + } + + doCopy() { + this.copyToClipboard(this.value); + this.copyClick.emit(); + this.firstLoad = false; + this.copied = true; + setTimeout(() => { + this.copied = false; + }, SHOW_CHECKBOX_TIMEOUT); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts index 45e43231d7..5657b167ad 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts @@ -6,40 +6,40 @@ /* eslint-disable */ export const TRANSLATIONS = { - en: { - copied: 'Copied' - }, - de: { - copied: 'Kopiert' - }, - es: { - copied: 'Copiado' - }, - fr: { - copied: 'Copié' - }, - ja: { - copied: 'コピー' - }, - ko: { - copied: '복사' - }, - zh_TW: { - copied: '已复制' - }, - zh_CN: { - copied: '已復制' - }, - it: { - copied: 'Copiato' - }, - nl: { - copied: 'Gekopieerd' - }, - pt: { - copied: 'Copiado' - }, - ru: { - copied: 'Скопировано' - } + en: { + copied: "Copied", + }, + de: { + copied: "Kopiert", + }, + es: { + copied: "Copiado", + }, + fr: { + copied: "Copié", + }, + ja: { + copied: "コピー", + }, + ko: { + copied: "복사", + }, + zh_TW: { + copied: "已复制", + }, + zh_CN: { + copied: "已復制", + }, + it: { + copied: "Copiato", + }, + nl: { + copied: "Gekopieerd", + }, + pt: { + copied: "Copiado", + }, + ru: { + copied: "Скопировано", + }, }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts index 0bf7eb027b..7d4b9d9818 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts @@ -5,10 +5,12 @@ /* eslint-disable */ -import { Type } from '@angular/core'; +import { Type } from "@angular/core"; -import { VdkCopyToClipboardButtonComponent } from './copy-to-clipboard-button.component'; +import { VdkCopyToClipboardButtonComponent } from "./copy-to-clipboard-button.component"; -export * from './copy-to-clipboard-button.component'; +export * from "./copy-to-clipboard-button.component"; -export const COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES: Type[] = [VdkCopyToClipboardButtonComponent]; +export const COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES: Type[] = [ + VdkCopyToClipboardButtonComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html index b987f2b52a..b4fb3821e2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html @@ -9,15 +9,15 @@
    - {{title}} + {{ title }}
    - {{description}} + {{ description }}
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss index 63e2d91fcd..d2fbcf7405 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss @@ -7,43 +7,43 @@ $text-dark: #919fa8; $text-light: #67747d; :host { - display: flex; - align-items: center; - flex-direction: column; - margin: 1rem 0; - - cds-icon { - fill: #ccc; - } - - .empty-placeholder-heading { - font-size: 18px; - color: $text-light; - margin-top: 18px; - line-height: 48px; - font-weight: 200; - } - - .empty-placeholder-description { - margin-top: 0; - font-size: 13px; - color: $text-light; - margin-bottom: 18px; - font-weight: 200; - } + display: flex; + align-items: center; + flex-direction: column; + margin: 1rem 0; + + cds-icon { + fill: #ccc; + } + + .empty-placeholder-heading { + font-size: 18px; + color: $text-light; + margin-top: 18px; + line-height: 48px; + font-weight: 200; + } + + .empty-placeholder-description { + margin-top: 0; + font-size: 13px; + color: $text-light; + margin-bottom: 18px; + font-weight: 200; + } } :host ::ng-deep button:last-child { - margin-right: 0 !important; + margin-right: 0 !important; } :host ::ng-deep button.btn-link { - margin-top: 0 !important; + margin-top: 0 !important; } :host-context(.dark) { - h2, - h3 { - color: $text-dark; - } + h2, + h3 { + color: $text-dark; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts index 19ae75ca4c..f21a03bbdb 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts @@ -5,16 +5,16 @@ /* eslint-disable */ -import { Component, Input } from '@angular/core'; +import { Component, Input } from "@angular/core"; @Component({ - selector: 'vdk-empty-state-placeholder', - templateUrl: './empty-state-placeholder.component.html', - styleUrls: ['./empty-state-placeholder.component.scss'] + selector: "vdk-empty-state-placeholder", + templateUrl: "./empty-state-placeholder.component.html", + styleUrls: ["./empty-state-placeholder.component.scss"], }) export class VdkEmptyStatePlaceholderComponent { - @Input('title') title: string; - @Input() icon: string; - @Input() description: string; - @Input() headingLevel = 2; + @Input("title") title: string; + @Input() icon: string; + @Input() description: string; + @Input() headingLevel = 2; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts index 4e05dd9ab7..2c7d2c3876 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommonModule } from '@angular/common'; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { VdkEmptyStatePlaceholderComponent } from './empty-state-placeholder.component'; +import { CommonModule } from "@angular/common"; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; +import { VdkEmptyStatePlaceholderComponent } from "./empty-state-placeholder.component"; @NgModule({ - imports: [CommonModule], - declarations: [VdkEmptyStatePlaceholderComponent], - exports: [VdkEmptyStatePlaceholderComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + imports: [CommonModule], + declarations: [VdkEmptyStatePlaceholderComponent], + exports: [VdkEmptyStatePlaceholderComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class VdkEmptyStatePlaceholderModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts index ed95c7f407..15953e3d85 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './empty-state-placeholder.component'; -export * from './empty-state-placeholder.module'; +export * from "./empty-state-placeholder.component"; +export * from "./empty-state-placeholder.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html index ae55efe946..fffcfb72fd 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html @@ -6,57 +6,68 @@ -
    - +
    + - -
    - -
    - -
    + {{ editBtn }} + +
    -
    - -
    +
    + +
    + +
    + +
    - +
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss index af9d38aa22..bdda9a87ed 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss @@ -4,12 +4,12 @@ */ :host ::ng-deep { - .section-title { - display: inline-block; - } + .section-title { + display: inline-block; + } } .csp-edit-button { - margin: 0 0; - cursor: pointer; + margin: 0 0; + cursor: pointer; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts index 99963918fa..e8354490bb 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts @@ -5,18 +5,26 @@ /* eslint-disable */ -import { VdkFormSectionContainerComponent } from './form-section-container.component'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { Component, DebugElement, ViewChild } from '@angular/core'; -import { ReactiveFormsModule, FormBuilder, FormGroup, FormControl } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { By } from '@angular/platform-browser'; - -import { ClarityModule } from '@clr/angular'; - -import { FORM_STATE, VdkFormState } from './form-section-container.component'; -import { VdkFormSectionComponent } from '../form-section'; -import { VdkSimpleTranslateModule, VdkSimpleTranslateService } from '../../ngx-utils'; +import { VdkFormSectionContainerComponent } from "./form-section-container.component"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { Component, DebugElement, ViewChild } from "@angular/core"; +import { + ReactiveFormsModule, + FormBuilder, + FormGroup, + FormControl, +} from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { By } from "@angular/platform-browser"; + +import { ClarityModule } from "@clr/angular"; + +import { FORM_STATE, VdkFormState } from "./form-section-container.component"; +import { VdkFormSectionComponent } from "../form-section"; +import { + VdkSimpleTranslateModule, + VdkSimpleTranslateService, +} from "../../ngx-utils"; let fixture: ComponentFixture; let hostComp: TestHostComponent; @@ -24,145 +32,211 @@ let page: PageObject; let el: Element; class PageObject { - fixtureElement: DebugElement; + fixtureElement: DebugElement; - constructor() {} + constructor() {} - populateLocators(): void { - this.fixtureElement = fixture.debugElement.query(By.css('vdk-form-section-container')); - } + populateLocators(): void { + this.fixtureElement = fixture.debugElement.query( + By.css("vdk-form-section-container"), + ); + } - getReadOnlySectionElements(): NodeListOf { - return el.querySelectorAll('.form-section-readonly'); - } + getReadOnlySectionElements(): NodeListOf { + return el.querySelectorAll(".form-section-readonly"); + } - getEditSectionElements(): NodeListOf { - return el.querySelectorAll('.form-section-edit'); - } + getEditSectionElements(): NodeListOf { + return el.querySelectorAll(".form-section-edit"); + } - setInputValue(inputName: string, inputValue: any) { - hostComp.editProfileForm.controls[inputName].setValue(inputValue); - hostComp.editProfileForm.controls[inputName].markAsDirty(); - } + setInputValue(inputName: string, inputValue: any) { + hostComp.editProfileForm.controls[inputName].setValue(inputValue); + hostComp.editProfileForm.controls[inputName].markAsDirty(); + } - getSaveBtnSpan(): HTMLElement | undefined { - const span = fixture.debugElement.queryAll(By.css('.csp-save-button span')); - if (span && span.length > 0) { - return span[0].nativeElement; - } - return undefined; + getSaveBtnSpan(): HTMLElement | undefined { + const span = fixture.debugElement.queryAll(By.css(".csp-save-button span")); + if (span && span.length > 0) { + return span[0].nativeElement; } + return undefined; + } } @Component({ - template: `
    - -
    Title
    - -
    -
    -
    - -
    -
    - firstName -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    ` + template: `
    + +
    Title
    + +
    +
    +
    + +
    +
    + firstName +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    `, }) class TestHostComponent { - @ViewChild(VdkFormSectionContainerComponent, { static: false }) - child: VdkFormSectionContainerComponent; - formState: VdkFormState; + @ViewChild(VdkFormSectionContainerComponent, { static: false }) + child: VdkFormSectionContainerComponent; + formState: VdkFormState; - editProfileForm = new FormGroup({ - firstName: new FormControl() - }); + editProfileForm = new FormGroup({ + firstName: new FormControl(), + }); } -describe('Form Section Container Component', () => { - function createComponent() { - fixture = TestBed.createComponent(TestHostComponent); - hostComp = fixture.componentInstance; - el = fixture.nativeElement; - fixture.detectChanges(); - page = new PageObject(); - page.populateLocators(); - } - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ReactiveFormsModule, VdkSimpleTranslateModule, BrowserAnimationsModule, ClarityModule], - providers: [VdkSimpleTranslateService, FormBuilder], - declarations: [TestHostComponent, VdkFormSectionComponent, VdkFormSectionContainerComponent] - }); - - createComponent(); - fixture.detectChanges(); - })); - - it('should project .title content correctly', () => { - expect(el.querySelector('.section-title').textContent).toContain('Title'); +describe("Form Section Container Component", () => { + function createComponent() { + fixture = TestBed.createComponent(TestHostComponent); + hostComp = fixture.componentInstance; + el = fixture.nativeElement; + fixture.detectChanges(); + page = new PageObject(); + page.populateLocators(); + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + VdkSimpleTranslateModule, + BrowserAnimationsModule, + ClarityModule, + ], + providers: [VdkSimpleTranslateService, FormBuilder], + declarations: [ + TestHostComponent, + VdkFormSectionComponent, + VdkFormSectionContainerComponent, + ], }); - it('should readOnly and Edit sections to be toggled on Edit', () => { - expect(page.getReadOnlySectionElements().length).toBe(1, 'Readonly Section shown initially'); - expect(page.getEditSectionElements().length).toBe(0, 'Edit section hidden initially'); - expect(el.querySelectorAll('.csp-edit-button').length).toBe(1, 'Edit button shown initially'); - const editButton: HTMLInputElement = el.querySelector('.csp-edit-button'); - editButton.click(); - fixture.detectChanges(); - - expect(page.getReadOnlySectionElements().length).toBe(0, 'Readonly section hidden on Edit button'); - expect(page.getEditSectionElements().length).toBe(1, 'Edit section shown on Edit button'); - }); - - it('should readOnly and Edit sections to be toggled on Cancel', () => { - const editButton: HTMLInputElement = el.querySelector('.csp-edit-button'); - editButton.click(); - fixture.detectChanges(); - - expect(el.querySelectorAll('.csp-cancel-button').length).toBe(1, 'Cancel button to be shown'); - const cancelButton: HTMLInputElement = el.querySelector('.csp-cancel-button'); - cancelButton.click(); - fixture.detectChanges(); - - expect(page.getReadOnlySectionElements().length).toBe(1, 'Readonly section shown on Cancel'); - expect(page.getEditSectionElements().length).toBe(0, 'Edit section hidden on Cancel'); - }); - - it('should show Readonly and Edit section after save', () => { - const editButton: HTMLInputElement = el.querySelector('.csp-edit-button'); - editButton.click(); - fixture.detectChanges(); - - page.setInputValue('firstName', 'firstName'); - expect(el.querySelectorAll('.csp-save-button').length).toBe(1, 'Save button shown'); - const saveButton: HTMLInputElement = el.querySelector('.csp-save-button'); - expect(page.getSaveBtnSpan().innerText.trim()).toBe('Save', 'save btn shown'); - saveButton.click(); - fixture.detectChanges(); - - expect(saveButton.disabled).toEqual(true, 'Save to be disabled'); - hostComp.formState = new VdkFormState(FORM_STATE.CAN_EDIT); - fixture.detectChanges(); - - expect(page.getReadOnlySectionElements().length).toBe(1, 'Readonly Section shown'); - expect(page.getEditSectionElements().length).toBe(0, 'Edit Section hidden'); - }); + createComponent(); + fixture.detectChanges(); + })); + + it("should project .title content correctly", () => { + expect(el.querySelector(".section-title").textContent).toContain("Title"); + }); + + it("should readOnly and Edit sections to be toggled on Edit", () => { + expect(page.getReadOnlySectionElements().length).toBe( + 1, + "Readonly Section shown initially", + ); + expect(page.getEditSectionElements().length).toBe( + 0, + "Edit section hidden initially", + ); + expect(el.querySelectorAll(".csp-edit-button").length).toBe( + 1, + "Edit button shown initially", + ); + const editButton: HTMLInputElement = ( + el.querySelector(".csp-edit-button") + ); + editButton.click(); + fixture.detectChanges(); + + expect(page.getReadOnlySectionElements().length).toBe( + 0, + "Readonly section hidden on Edit button", + ); + expect(page.getEditSectionElements().length).toBe( + 1, + "Edit section shown on Edit button", + ); + }); + + it("should readOnly and Edit sections to be toggled on Cancel", () => { + const editButton: HTMLInputElement = ( + el.querySelector(".csp-edit-button") + ); + editButton.click(); + fixture.detectChanges(); + + expect(el.querySelectorAll(".csp-cancel-button").length).toBe( + 1, + "Cancel button to be shown", + ); + const cancelButton: HTMLInputElement = ( + el.querySelector(".csp-cancel-button") + ); + cancelButton.click(); + fixture.detectChanges(); + + expect(page.getReadOnlySectionElements().length).toBe( + 1, + "Readonly section shown on Cancel", + ); + expect(page.getEditSectionElements().length).toBe( + 0, + "Edit section hidden on Cancel", + ); + }); + + it("should show Readonly and Edit section after save", () => { + const editButton: HTMLInputElement = ( + el.querySelector(".csp-edit-button") + ); + editButton.click(); + fixture.detectChanges(); + + page.setInputValue("firstName", "firstName"); + expect(el.querySelectorAll(".csp-save-button").length).toBe( + 1, + "Save button shown", + ); + const saveButton: HTMLInputElement = ( + el.querySelector(".csp-save-button") + ); + expect(page.getSaveBtnSpan().innerText.trim()).toBe( + "Save", + "save btn shown", + ); + saveButton.click(); + fixture.detectChanges(); + + expect(saveButton.disabled).toEqual(true, "Save to be disabled"); + hostComp.formState = new VdkFormState(FORM_STATE.CAN_EDIT); + fixture.detectChanges(); + + expect(page.getReadOnlySectionElements().length).toBe( + 1, + "Readonly Section shown", + ); + expect(page.getEditSectionElements().length).toBe(0, "Edit Section hidden"); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts index 02fbe72140..476b87f564 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts @@ -6,171 +6,187 @@ /* eslint-disable */ import { - Component, - Output, - EventEmitter, - Input, - Host, - Optional, - ChangeDetectionStrategy, - ChangeDetectorRef, - ViewChild, - ElementRef -} from '@angular/core'; -import { FormGroupDirective } from '@angular/forms'; -import { ClrLoadingState } from '@clr/angular'; + Component, + Output, + EventEmitter, + Input, + Host, + Optional, + ChangeDetectionStrategy, + ChangeDetectorRef, + ViewChild, + ElementRef, +} from "@angular/core"; +import { FormGroupDirective } from "@angular/forms"; +import { ClrLoadingState } from "@clr/angular"; export enum FORM_STATE { - VIEW, - CAN_EDIT, - EDIT, - ERROR, - SUBMIT + VIEW, + CAN_EDIT, + EDIT, + ERROR, + SUBMIT, } export class VdkFormState { - state: FORM_STATE; - - /** - * Optional. The section with this name identifier will be excluded from the state change. - */ - emittingSection: string; - - /** - * Optional. - * All the sections in the array will change its state. - * All the others will be excluded if this array is not empty. - */ - sectionsToInclude: string[]; - - constructor(_state: FORM_STATE, _sectionsToInclude?: string[], _emittingSection?: string) { - this.state = _state; - this.sectionsToInclude = _sectionsToInclude ? _sectionsToInclude : []; - this.emittingSection = _emittingSection; - } + state: FORM_STATE; + + /** + * Optional. The section with this name identifier will be excluded from the state change. + */ + emittingSection: string; + + /** + * Optional. + * All the sections in the array will change its state. + * All the others will be excluded if this array is not empty. + */ + sectionsToInclude: string[]; + + constructor( + _state: FORM_STATE, + _sectionsToInclude?: string[], + _emittingSection?: string, + ) { + this.state = _state; + this.sectionsToInclude = _sectionsToInclude ? _sectionsToInclude : []; + this.emittingSection = _emittingSection; + } } @Component({ - selector: 'vdk-form-section-container', - templateUrl: './form-section-container.component.html', - styleUrls: ['form-section-container.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + selector: "vdk-form-section-container", + templateUrl: "./form-section-container.component.html", + styleUrls: ["form-section-container.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class VdkFormSectionContainerComponent { - FORM_STATE = FORM_STATE; //used in the template - _sectionState: FORM_STATE = FORM_STATE.CAN_EDIT; - ClrLoadingState = ClrLoadingState; - stopInitialFocus = true; - - @Input() canEditSection = true; //set to false if the section is readonly - @Input() isSubmitEnabled: boolean; //controls the Submit(Save) button state - @Input() sectionName: string; //unique section identifier - @Input() editBtn = 'Edit'; //Edit button text - @Input() cancelBtn = 'Cancel'; //Cancel button text - @Input() saveBtn = 'Save'; //Save button text - @Input() editBtnAriaLabel = 'Edit'; //Edit button text - @Input() cancelBtnAriaLabel = 'Cancel'; //Cancel button text - @Input() saveBtnAriaLabel = 'Save'; //Save button text - - @Input('formState') set formState(_formState: VdkFormState) { - if (!_formState) { - return; - } + FORM_STATE = FORM_STATE; //used in the template + _sectionState: FORM_STATE = FORM_STATE.CAN_EDIT; + ClrLoadingState = ClrLoadingState; + stopInitialFocus = true; + + @Input() canEditSection = true; //set to false if the section is readonly + @Input() isSubmitEnabled: boolean; //controls the Submit(Save) button state + @Input() sectionName: string; //unique section identifier + @Input() editBtn = "Edit"; //Edit button text + @Input() cancelBtn = "Cancel"; //Cancel button text + @Input() saveBtn = "Save"; //Save button text + @Input() editBtnAriaLabel = "Edit"; //Edit button text + @Input() cancelBtnAriaLabel = "Cancel"; //Cancel button text + @Input() saveBtnAriaLabel = "Save"; //Save button text + + @Input("formState") set formState(_formState: VdkFormState) { + if (!_formState) { + return; + } - if ( - (_formState.emittingSection && _formState.emittingSection !== this.sectionName) || - (!_formState.emittingSection && _formState.sectionsToInclude.length === 0) || - _formState.sectionsToInclude.some((name) => name === this.sectionName) - ) { - //on ERROR set EDIT state only on submitted section - //to the rest of sections restore CAN_EDIT state - if (_formState.state === FORM_STATE.ERROR) { - if (this._sectionState === FORM_STATE.SUBMIT) { - this._sectionState = FORM_STATE.EDIT; - //put it in a microtask(make it asynchronous) to not violate detection run - //and avoid error for Expression changed after check - Promise.resolve(null).then(() => { - if (this.cspForm) { - this.cspForm.form.enable(); - } else { - this.enableForm.emit(); - } - }); - } else { - this._sectionState = FORM_STATE.CAN_EDIT; - } + if ( + (_formState.emittingSection && + _formState.emittingSection !== this.sectionName) || + (!_formState.emittingSection && + _formState.sectionsToInclude.length === 0) || + _formState.sectionsToInclude.some((name) => name === this.sectionName) + ) { + //on ERROR set EDIT state only on submitted section + //to the rest of sections restore CAN_EDIT state + if (_formState.state === FORM_STATE.ERROR) { + if (this._sectionState === FORM_STATE.SUBMIT) { + this._sectionState = FORM_STATE.EDIT; + //put it in a microtask(make it asynchronous) to not violate detection run + //and avoid error for Expression changed after check + Promise.resolve(null).then(() => { + if (this.cspForm) { + this.cspForm.form.enable(); } else { - this.changeSectionState(_formState.state); + this.enableForm.emit(); } - } - } - - @Output() formStateChange = new EventEmitter(); - @Output() sectionStateChange = new EventEmitter(); - - //Events that are used when no formGroup is found in parent component - @Output() submitForm = new EventEmitter(); - @Output() disableForm = new EventEmitter(); - @Output() enableForm = new EventEmitter(); - - @ViewChild('editButton') editButtonEl: ElementRef; - - constructor( - @Optional() @Host() private cspForm: FormGroupDirective, - private cdr: ChangeDetectorRef - ) {} - - showEditBtn() { - return this._sectionState === FORM_STATE.CAN_EDIT && this.canEditSection; - } - - showSaveBtn() { - return this._sectionState === FORM_STATE.EDIT; - } - - clickEdit() { - this.formStateChange.emit(new VdkFormState(FORM_STATE.CAN_EDIT, [], this.sectionName)); - if (this.cspForm) { - this.cspForm.form.enable(); + }); } else { - this.enableForm.emit(); + this._sectionState = FORM_STATE.CAN_EDIT; } - this.changeSectionState(FORM_STATE.EDIT); + } else { + this.changeSectionState(_formState.state); + } } - - clickCancel() { - this.changeSectionState(FORM_STATE.CAN_EDIT); + } + + @Output() formStateChange = new EventEmitter(); + @Output() sectionStateChange = new EventEmitter(); + + //Events that are used when no formGroup is found in parent component + @Output() submitForm = new EventEmitter(); + @Output() disableForm = new EventEmitter(); + @Output() enableForm = new EventEmitter(); + + @ViewChild("editButton") editButtonEl: ElementRef; + + constructor( + @Optional() @Host() private cspForm: FormGroupDirective, + private cdr: ChangeDetectorRef, + ) {} + + showEditBtn() { + return this._sectionState === FORM_STATE.CAN_EDIT && this.canEditSection; + } + + showSaveBtn() { + return this._sectionState === FORM_STATE.EDIT; + } + + clickEdit() { + this.formStateChange.emit( + new VdkFormState(FORM_STATE.CAN_EDIT, [], this.sectionName), + ); + if (this.cspForm) { + this.cspForm.form.enable(); + } else { + this.enableForm.emit(); } - - // @ts-ignore - clickSave(): boolean { - this.formStateChange.emit(new VdkFormState(FORM_STATE.VIEW, [], this.sectionName)); - this.changeSectionState(FORM_STATE.SUBMIT); - if (this.cspForm) { - this.cspForm.form.disable(); - this.cspForm.onSubmit(this.cspForm.value); - this.cspForm.form.markAsPristine(); - // cancel submitting the form since cspForm.onSubmit has been called above. - // solves a specific issue in Firefox where onSubmit was called twice. - return false; - } else { - this.disableForm.emit(); - this.submitForm.emit(); - } + this.changeSectionState(FORM_STATE.EDIT); + } + + clickCancel() { + this.changeSectionState(FORM_STATE.CAN_EDIT); + } + + // @ts-ignore + clickSave(): boolean { + this.formStateChange.emit( + new VdkFormState(FORM_STATE.VIEW, [], this.sectionName), + ); + this.changeSectionState(FORM_STATE.SUBMIT); + if (this.cspForm) { + this.cspForm.form.disable(); + this.cspForm.onSubmit(this.cspForm.value); + this.cspForm.form.markAsPristine(); + // cancel submitting the form since cspForm.onSubmit has been called above. + // solves a specific issue in Firefox where onSubmit was called twice. + return false; + } else { + this.disableForm.emit(); + this.submitForm.emit(); } - - changeSectionState(_sectionState: FORM_STATE) { - if (_sectionState !== this._sectionState) { - this.sectionStateChange.emit(new VdkFormState(_sectionState, [], this.sectionName)); - this._sectionState = _sectionState; - } + } + + changeSectionState(_sectionState: FORM_STATE) { + if (_sectionState !== this._sectionState) { + this.sectionStateChange.emit( + new VdkFormState(_sectionState, [], this.sectionName), + ); + this._sectionState = _sectionState; } - - focusEdit() { - if (this.editButtonEl && this._sectionState === FORM_STATE.CAN_EDIT && !this.stopInitialFocus) { - this.editButtonEl.nativeElement.focus(); - } - if (this.stopInitialFocus) { - this.stopInitialFocus = false; - } + } + + focusEdit() { + if ( + this.editButtonEl && + this._sectionState === FORM_STATE.CAN_EDIT && + !this.stopInitialFocus + ) { + this.editButtonEl.nativeElement.focus(); + } + if (this.stopInitialFocus) { + this.stopInitialFocus = false; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts index 5c8c7eb66f..be322189b8 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts @@ -5,10 +5,12 @@ /* eslint-disable */ -import { Type } from '@angular/core'; +import { Type } from "@angular/core"; -import { VdkFormSectionContainerComponent } from './form-section-container.component'; +import { VdkFormSectionContainerComponent } from "./form-section-container.component"; -export * from './form-section-container.component'; +export * from "./form-section-container.component"; -export const FORM_SECTION_CONTAINER_DIRECTIVES: Type[] = [VdkFormSectionContainerComponent]; +export const FORM_SECTION_CONTAINER_DIRECTIVES: Type[] = [ + VdkFormSectionContainerComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html index 12dd342b11..57e516bb20 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html @@ -6,18 +6,18 @@
    -
    -
    -
    - -
    - -
    - -
    +
    +
    +
    +
    + +
    + +
    +
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss index 460f47ef53..bfeb196f48 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss @@ -4,35 +4,35 @@ */ .vdk-form-container { - display: flex; + display: flex; + + .vdk-vertical-line { + margin-top: 20px; + margin-bottom: 5px; + border-left: 3px solid #007cbb; + } - .vdk-vertical-line { - margin-top: 20px; - margin-bottom: 5px; - border-left: 3px solid #007cbb; + .vdk-form-content { + .form-header { + padding-bottom: 15px; } - .vdk-form-content { - .form-header { - padding-bottom: 15px; - } + display: flex; + flex-direction: column; + width: 100%; + } - display: flex; - flex-direction: column; - width: 100%; - } + .form-header { + color: #000; + font-size: 18px; + font-weight: 200; + margin-block-start: 24px; + margin-top: 24px; + } + :host-context(.dark) { .form-header { - color: #000; - font-size: 18px; - font-weight: 200; - margin-block-start: 24px; - margin-top: 24px; - } - - :host-context(.dark) { - .form-header { - color: #eaedf0; - } + color: #eaedf0; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts index 592a204478..0662b7e743 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts @@ -5,70 +5,78 @@ /* eslint-disable */ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { Component, ViewChild } from "@angular/core"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { VdkFormSectionComponent } from './form-section.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { VdkFormSectionComponent } from "./form-section.component"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @Component({ - template: ` - - Test header - Test content - - - ` + template: ` + + Test header + Test content + + + `, }) class TestHostComponent { - @ViewChild(VdkFormSectionComponent, { static: true }) - child: VdkFormSectionComponent; - focused: boolean = false; + @ViewChild(VdkFormSectionComponent, { static: true }) + child: VdkFormSectionComponent; + focused: boolean = false; } -describe('VdkFormSectionComponent', () => { - let fixture: ComponentFixture; - let comp: VdkFormSectionComponent; - let el: any; +describe("VdkFormSectionComponent", () => { + let fixture: ComponentFixture; + let comp: VdkFormSectionComponent; + let el: any; - function createComponent() { - fixture = TestBed.createComponent(TestHostComponent); - comp = fixture.componentInstance.child; - el = fixture.debugElement.nativeElement; - fixture.detectChanges(); - } + function createComponent() { + fixture = TestBed.createComponent(TestHostComponent); + comp = fixture.componentInstance.child; + el = fixture.debugElement.nativeElement; + fixture.detectChanges(); + } - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, ClarityModule], - providers: [], - declarations: [TestHostComponent, VdkFormSectionComponent] - }); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, ClarityModule], + providers: [], + declarations: [TestHostComponent, VdkFormSectionComponent], + }); - createComponent(); - fixture.detectChanges(); - })); + createComponent(); + fixture.detectChanges(); + })); - it('should project .form-section-header content correctly', () => { - const headers: NodeListOf = el.querySelectorAll('vdk-form-section span.form-section-header'); - expect(headers.length).toBe(1); - }); + it("should project .form-section-header content correctly", () => { + const headers: NodeListOf = el.querySelectorAll( + "vdk-form-section span.form-section-header", + ); + expect(headers.length).toBe(1); + }); - it('should project .form-section-content content correctly', () => { - const content: NodeListOf = el.querySelectorAll('vdk-form-section span.form-section-content'); - expect(content.length).toBe(1); - }); + it("should project .form-section-content content correctly", () => { + const content: NodeListOf = el.querySelectorAll( + "vdk-form-section span.form-section-content", + ); + expect(content.length).toBe(1); + }); - it('should project .form-section-footer content only when focused and then change edit state', () => { - expect(comp.getFormState()).toBe('normal'); - const footer: NodeListOf = el.querySelectorAll('vdk-form-section span.form-section-footer'); - expect(footer.length).toBe(0); - fixture.componentInstance.focused = true; - fixture.detectChanges(); - expect(comp.getFormState()).toBe('edit'); - const footers: NodeListOf = el.querySelectorAll('vdk-form-section span.form-section-footer'); - expect(footers.length).toBe(1); - }); + it("should project .form-section-footer content only when focused and then change edit state", () => { + expect(comp.getFormState()).toBe("normal"); + const footer: NodeListOf = el.querySelectorAll( + "vdk-form-section span.form-section-footer", + ); + expect(footer.length).toBe(0); + fixture.componentInstance.focused = true; + fixture.detectChanges(); + expect(comp.getFormState()).toBe("edit"); + const footers: NodeListOf = el.querySelectorAll( + "vdk-form-section span.form-section-footer", + ); + expect(footers.length).toBe(1); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts index 5617a2f96d..9117d6e66c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts @@ -5,55 +5,63 @@ /* eslint-disable */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { animate, state, style, transition, trigger } from '@angular/animations'; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { + animate, + state, + style, + transition, + trigger, +} from "@angular/animations"; @Component({ - selector: 'vdk-form-section', - styleUrls: ['form-section.component.scss'], - templateUrl: 'form-section.component.html', - animations: [ - trigger('customFormState', [ - state( - 'edit', - style({ - opacity: 1, - width: '3px', - 'margin-right': '24px' - }) - ), - state( - 'normal', - style({ - opacity: 0, - width: '0px', - 'margin-right': '0px' - }) - ), - transition('normal => edit', [animate('300ms ease-in-out')]), - transition('edit => normal', [animate('100ms ease-in-out')]) - ]), - trigger('footerState', [ - transition(':enter', [ - style({ opacity: 0, height: '0' }), - animate('0.1s 0.2s ease-in-out', style({ opacity: 1, height: '*' })) - ]), - transition(':leave', [animate('0.1s', style({ opacity: 0, height: '0' }))]) - ]) - ] + selector: "vdk-form-section", + styleUrls: ["form-section.component.scss"], + templateUrl: "form-section.component.html", + animations: [ + trigger("customFormState", [ + state( + "edit", + style({ + opacity: 1, + width: "3px", + "margin-right": "24px", + }), + ), + state( + "normal", + style({ + opacity: 0, + width: "0px", + "margin-right": "0px", + }), + ), + transition("normal => edit", [animate("300ms ease-in-out")]), + transition("edit => normal", [animate("100ms ease-in-out")]), + ]), + trigger("footerState", [ + transition(":enter", [ + style({ opacity: 0, height: "0" }), + animate("0.1s 0.2s ease-in-out", style({ opacity: 1, height: "*" })), + ]), + transition(":leave", [ + animate("0.1s", style({ opacity: 0, height: "0" })), + ]), + ]), + ], }) export class VdkFormSectionComponent { - @Input() - focused: boolean = false; + @Input() + focused: boolean = false; - @Output() - animationDone = new EventEmitter(); + @Output() + animationDone = new EventEmitter(); - getFormState() { - return this.focused ? 'edit' : 'normal'; - } + getFormState() { + return this.focused ? "edit" : "normal"; + } - emitAnimationDone() { - this.animationDone.emit(); - } + emitAnimationDone() { + this.animationDone.emit(); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts index ccbb3708e1..356ded0238 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts @@ -5,10 +5,10 @@ /* eslint-disable */ -import { Type } from '@angular/core'; +import { Type } from "@angular/core"; -import { VdkFormSectionComponent } from './form-section.component'; +import { VdkFormSectionComponent } from "./form-section.component"; -export * from './form-section.component'; +export * from "./form-section.component"; export const FORM_SECTION_DIRECTIVES: Type[] = [VdkFormSectionComponent]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts index 98355703d2..59d62e2b1f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './copy-to-clipboard-button/index'; -export * from './form-section/index'; -export * from './form-section-container/index'; -export * from './empty-state-placeholder/index'; -export * from './toast/index'; -export * from './search/index'; +export * from "./copy-to-clipboard-button/index"; +export * from "./form-section/index"; +export * from "./form-section-container/index"; +export * from "./empty-state-placeholder/index"; +export * from "./toast/index"; +export * from "./search/index"; -export * from './vdk-ngx-components.module'; +export * from "./vdk-ngx-components.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts index ce8ee936d7..9d28d96bce 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './search.component'; -export { VdkSearchModule } from './search.module'; +export * from "./search.component"; +export { VdkSearchModule } from "./search.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html index e0307889d3..9c9e0f30e6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html @@ -4,60 +4,65 @@ -->
    - - + + - + - - + + - - - - + + + + - {{helperText}} + {{ + helperText + }}
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss index ae7fa0f1c0..0c25061f74 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss @@ -4,92 +4,92 @@ */ :host { - width: 200px; - display: block; + width: 200px; + display: block; } .search-container { - display: flex; - flex-direction: column; - position: relative; - width: 100%; - margin-bottom: 0; - margin: 0.5rem 0; + display: flex; + flex-direction: column; + position: relative; + width: 100%; + margin-bottom: 0; + margin: 0.5rem 0; - &.disabled { - cursor: not-allowed; + &.disabled { + cursor: not-allowed; - * { - pointer-events: none; - } - - input { - color: #888888; - } - - cds-icon[shape='search'] { - fill: #888888; - } - - .clear-search-btn { - cds-icon[shape='times-circle'] { - fill: #888888; - } - } + * { + pointer-events: none; } - cds-icon[shape='search'] { - height: 18px; - position: absolute; - top: 3px; - left: 0; + input { + color: #888888; } - &.focused { - cds-icon[shape='search'] { - fill: var(--clr-forms-focused-color); - stroke: var(--clr-forms-focused-color); - stroke-width: 0.75px; - } + cds-icon[shape="search"] { + fill: #888888; } .clear-search-btn { - position: absolute; - top: -1px; - right: 0; - padding: 0; - border: transparent; - background-color: transparent; - - cds-icon[shape='times-circle'] { - fill: #565656; - opacity: 0.8; - &:hover { - cursor: pointer; - opacity: 1; - } - } + cds-icon[shape="times-circle"] { + fill: #888888; + } } + } + + cds-icon[shape="search"] { + height: 18px; + position: absolute; + top: 3px; + left: 0; + } - .btn-link { - position: absolute; - right: 0; - top: -6px; - margin: 0; + &.focused { + cds-icon[shape="search"] { + fill: var(--clr-forms-focused-color); + stroke: var(--clr-forms-focused-color); + stroke-width: 0.75px; } + } - input { - padding: 0 24px; - flex: 1 1 24px; + .clear-search-btn { + position: absolute; + top: -1px; + right: 0; + padding: 0; + border: transparent; + background-color: transparent; + + cds-icon[shape="times-circle"] { + fill: #565656; + opacity: 0.8; + &:hover { + cursor: pointer; + opacity: 1; + } } + } + + .btn-link { + position: absolute; + right: 0; + top: -6px; + margin: 0; + } + + input { + padding: 0 24px; + flex: 1 1 24px; + } } :host-context(.dark) { - .clear-search-btn { - color: #d3d3d3; + .clear-search-btn { + color: #d3d3d3; - cds-icon[shape='times-circle'] { - fill: #acbac3; - } + cds-icon[shape="times-circle"] { + fill: #acbac3; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts index a88f74747d..c0a07aa253 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts @@ -5,216 +5,241 @@ /* eslint-disable */ -import { ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { ClarityModule } from '@clr/angular'; -import { VdkSearchComponent } from './search.component'; +import { + ComponentFixture, + ComponentFixtureAutoDetect, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from "@angular/core/testing"; +import { ReactiveFormsModule } from "@angular/forms"; + +import { ClarityModule } from "@clr/angular"; +import { VdkSearchComponent } from "./search.component"; export class PageObject { - constructor(private readonly fixture: ComponentFixture) {} - - getSearchInputElement(): HTMLInputElement { - return this.fixture.nativeElement.querySelector(`[data-test-id="search-input"]`); - } - - getSearchButtonElement(): HTMLInputElement { - return this.fixture.nativeElement.querySelector(`[data-test-id="search-button"]`); - } - - getClearSearchBtn(): HTMLButtonElement { - return this.fixture.nativeElement.querySelector(`[data-test-id="clear-search-btn"]`); - } - - getSearchIcon(): HTMLElement { - return this.fixture.nativeElement.querySelector(`[data-test-id="search-icon"]`); - } - - getHelperText(): HTMLElement { - return this.fixture.nativeElement.querySelector('[data-test-id="search-results-text"]'); - } + constructor(private readonly fixture: ComponentFixture) {} + + getSearchInputElement(): HTMLInputElement { + return this.fixture.nativeElement.querySelector( + `[data-test-id="search-input"]`, + ); + } + + getSearchButtonElement(): HTMLInputElement { + return this.fixture.nativeElement.querySelector( + `[data-test-id="search-button"]`, + ); + } + + getClearSearchBtn(): HTMLButtonElement { + return this.fixture.nativeElement.querySelector( + `[data-test-id="clear-search-btn"]`, + ); + } + + getSearchIcon(): HTMLElement { + return this.fixture.nativeElement.querySelector( + `[data-test-id="search-icon"]`, + ); + } + + getHelperText(): HTMLElement { + return this.fixture.nativeElement.querySelector( + '[data-test-id="search-results-text"]', + ); + } } -describe('VdkSearchComponent', () => { - let comp: VdkSearchComponent; - let fixture: ComponentFixture; - let page: PageObject; +describe("VdkSearchComponent", () => { + let comp: VdkSearchComponent; + let fixture: ComponentFixture; + let page: PageObject; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ClarityModule, ReactiveFormsModule], - declarations: [VdkSearchComponent], - providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }] - }); - - fixture = TestBed.createComponent(VdkSearchComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - page = new PageObject(fixture); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ClarityModule, ReactiveFormsModule], + declarations: [VdkSearchComponent], + providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }], }); - it('should create', waitForAsync(async () => { - expect(comp).toBeTruthy(); - await fixture.whenStable(); - expect(page.getSearchInputElement()).toBeTruthy(); - expect(page.getSearchIcon()).toBeTruthy(); - expect(page.getClearSearchBtn()).toBeFalsy(); - })); - - it('should show and hide "clear search button"', waitForAsync(async () => { - const searchQuery = 'test'; - expect(page.getClearSearchBtn()).toBeFalsy(); - page.getSearchInputElement().value = searchQuery; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - await fixture.whenStable().then(() => { - expect(comp.searchQuery.value).toBe(searchQuery, 'searchQuery input value is not correct'); - expect(comp.searchQueryValue).toBe(searchQuery, 'searchQueryValue property is not correct'); - expect(page.getClearSearchBtn()).toBeTruthy(); - page.getClearSearchBtn().click(); - fixture.detectChanges(); - // return fixture.whenStable(); - }); - // .then(() => { - // expect(page.getClearSearchBtn()).toBeFalsy(); - // }); - })); - - it('should input show correct data', fakeAsync(() => { - const searchQuery = 'test'; - page.getSearchInputElement().value = searchQuery; - page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture = TestBed.createComponent(VdkSearchComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + page = new PageObject(fixture); + }); + + it("should create", waitForAsync(async () => { + expect(comp).toBeTruthy(); + await fixture.whenStable(); + expect(page.getSearchInputElement()).toBeTruthy(); + expect(page.getSearchIcon()).toBeTruthy(); + expect(page.getClearSearchBtn()).toBeFalsy(); + })); + + it('should show and hide "clear search button"', waitForAsync(async () => { + const searchQuery = "test"; + expect(page.getClearSearchBtn()).toBeFalsy(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable().then(() => { + expect(comp.searchQuery.value).toBe( + searchQuery, + "searchQuery input value is not correct", + ); + expect(comp.searchQueryValue).toBe( + searchQuery, + "searchQueryValue property is not correct", + ); + expect(page.getClearSearchBtn()).toBeTruthy(); + page.getClearSearchBtn().click(); + fixture.detectChanges(); + // return fixture.whenStable(); + }); + // .then(() => { + // expect(page.getClearSearchBtn()).toBeFalsy(); + // }); + })); + + it("should input show correct data", fakeAsync(() => { + const searchQuery = "test"; + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + fixture + .whenStable() + .then(() => { + expect(page.getSearchInputElement().value).toBe(searchQuery); + page.getClearSearchBtn().click(); fixture.detectChanges(); - fixture - .whenStable() - .then(() => { - expect(page.getSearchInputElement().value).toBe(searchQuery); - page.getClearSearchBtn().click(); - fixture.detectChanges(); - return fixture.whenStable(); - }) - .then(() => { - expect(comp.searchQueryValue).toBe(''); - }); - - tick(1000); - })); + return fixture.whenStable(); + }) + .then(() => { + expect(comp.searchQueryValue).toBe(""); + }); + + tick(1000); + })); + + it("should emit correct value", fakeAsync(() => { + const searchQuery = "test"; + const searchSpy = spyOn(comp.search, "emit").and.callThrough(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(searchSpy).toHaveBeenCalledWith(searchQuery); + }); - it('should emit correct value', fakeAsync(() => { - const searchQuery = 'test'; - const searchSpy = spyOn(comp.search, 'emit').and.callThrough(); - page.getSearchInputElement().value = searchQuery; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(searchSpy).toHaveBeenCalledWith(searchQuery); - }); + tick(1000); + })); - tick(1000); - })); + it("should show helper text with results when resultCount is a number", waitForAsync(async () => { + fixture.componentInstance.helperText = `Over 9999 results`; + fixture.detectChanges(); + await fixture.whenStable(); + expect(page.getHelperText()).not.toBeNull(); + })); - it('should show helper text with results when resultCount is a number', waitForAsync(async () => { - fixture.componentInstance.helperText = `Over 9999 results`; - fixture.detectChanges(); - await fixture.whenStable(); - expect(page.getHelperText()).not.toBeNull(); - })); + it("should not show helper text with results when resultCount is not used", () => { + expect(page.getHelperText()).toBeNull(); + }); - it('should not show helper text with results when resultCount is not used', () => { - expect(page.getHelperText()).toBeNull(); + describe('when in "Manual Search" mode', () => { + beforeEach(() => { + comp.showSearchButton = true; }); - describe('when in "Manual Search" mode', () => { - beforeEach(() => { - comp.showSearchButton = true; - }); - - it('should show "SEARCH" button disabled when more than one char is entered but it is not above the min limit', waitForAsync(async () => { - comp.searchTermMinimalLength = 3; + it('should show "SEARCH" button disabled when more than one char is entered but it is not above the min limit', waitForAsync(async () => { + comp.searchTermMinimalLength = 3; - fixture.detectChanges(); + fixture.detectChanges(); - expect(page.getSearchButtonElement()).toBeNull(); + expect(page.getSearchButtonElement()).toBeNull(); - page.getSearchInputElement().value = 't'; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - await fixture.whenStable(); + page.getSearchInputElement().value = "t"; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); - let searchButton = page.getSearchButtonElement(); - expect(searchButton).toBeTruthy(); - expect(searchButton.disabled).toBeTruthy(); + let searchButton = page.getSearchButtonElement(); + expect(searchButton).toBeTruthy(); + expect(searchButton.disabled).toBeTruthy(); - page.getSearchInputElement().value = 'two'; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - await fixture.whenStable(); + page.getSearchInputElement().value = "two"; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); - searchButton = page.getSearchButtonElement(); - expect(searchButton).toBeTruthy(); - expect(searchButton.disabled).toBeFalsy(); - })); + searchButton = page.getSearchButtonElement(); + expect(searchButton).toBeTruthy(); + expect(searchButton.disabled).toBeFalsy(); + })); - xit('should emit value ONLY upon clicking "Enter" or "SEARCH" and should hide the latter', waitForAsync(async () => { - const searchQuery = 'test'; - const newSearchQuery = searchQuery + '123'; - const searchSpy = spyOn(comp.search, 'emit').and.callThrough(); - page.getSearchInputElement().value = searchQuery; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - await fixture.whenStable(); + xit('should emit value ONLY upon clicking "Enter" or "SEARCH" and should hide the latter', waitForAsync(async () => { + const searchQuery = "test"; + const newSearchQuery = searchQuery + "123"; + const searchSpy = spyOn(comp.search, "emit").and.callThrough(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); - expect(page.getClearSearchBtn()).toBeFalsy(); - expect(searchSpy).not.toHaveBeenCalledWith(searchQuery); + expect(page.getClearSearchBtn()).toBeFalsy(); + expect(searchSpy).not.toHaveBeenCalledWith(searchQuery); - const searchButton = page.getSearchButtonElement(); - searchButton.click(); + const searchButton = page.getSearchButtonElement(); + searchButton.click(); - await fixture.whenStable(); + await fixture.whenStable(); - expect(searchSpy).toHaveBeenCalledWith(searchQuery); + expect(searchSpy).toHaveBeenCalledWith(searchQuery); - expect(page.getClearSearchBtn()).toBeTruthy(); + expect(page.getClearSearchBtn()).toBeTruthy(); - page.getSearchInputElement().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + page + .getSearchInputElement() + .dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); - page.getSearchInputElement().value = newSearchQuery; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - await fixture.whenStable(); + page.getSearchInputElement().value = newSearchQuery; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); - expect(searchSpy).toHaveBeenCalledWith(newSearchQuery); - })); + expect(searchSpy).toHaveBeenCalledWith(newSearchQuery); + })); - xit('should clear input upon clicking "X" button', waitForAsync(async () => { - const searchQuery = 'test'; - const searchSpy = spyOn(comp.search, 'emit').and.callThrough(); + xit('should clear input upon clicking "X" button', waitForAsync(async () => { + const searchQuery = "test"; + const searchSpy = spyOn(comp.search, "emit").and.callThrough(); - page.getSearchInputElement().value = searchQuery; - page.getSearchInputElement().dispatchEvent(new Event('input')); - fixture.detectChanges(); - await fixture.whenStable(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event("input")); + fixture.detectChanges(); + await fixture.whenStable(); - expect(searchSpy).not.toHaveBeenCalledWith(searchQuery); + expect(searchSpy).not.toHaveBeenCalledWith(searchQuery); - let searchButton = page.getSearchButtonElement(); - searchButton.click(); - await fixture.whenStable(); + let searchButton = page.getSearchButtonElement(); + searchButton.click(); + await fixture.whenStable(); - expect(searchSpy).toHaveBeenCalledWith(searchQuery); + expect(searchSpy).toHaveBeenCalledWith(searchQuery); - searchButton = page.getSearchButtonElement(); - expect(searchButton).toBeNull(); + searchButton = page.getSearchButtonElement(); + expect(searchButton).toBeNull(); - const clearSearchButton = page.getClearSearchBtn(); - expect(clearSearchButton).toBeTruthy(); + const clearSearchButton = page.getClearSearchBtn(); + expect(clearSearchButton).toBeTruthy(); - clearSearchButton.click(); - fixture.detectChanges(); - await fixture.whenStable(); + clearSearchButton.click(); + fixture.detectChanges(); + await fixture.whenStable(); - expect(comp.searchQueryValue).toEqual(''); - expect(searchSpy).toHaveBeenCalledWith(''); - })); - }); + expect(comp.searchQueryValue).toEqual(""); + expect(searchSpy).toHaveBeenCalledWith(""); + })); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts index b4857dcd39..40d2bdfc03 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts @@ -5,131 +5,147 @@ /* eslint-disable */ -import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { FormControl } from '@angular/forms'; - -import { BehaviorSubject, combineLatest, Subject, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from "@angular/core"; +import { FormControl } from "@angular/forms"; + +import { BehaviorSubject, combineLatest, Subject, Subscription } from "rxjs"; +import { debounceTime } from "rxjs/operators"; const defaultSearchInputPadding = 24; @Component({ - selector: 'vdk-search', - templateUrl: './search.component.html', - styleUrls: ['./search.component.scss'] + selector: "vdk-search", + templateUrl: "./search.component.html", + styleUrls: ["./search.component.scss"], }) export class VdkSearchComponent { - searchInputPaddingRight = `${defaultSearchInputPadding}px`; - - private _disabled: boolean = false; - public get disabled(): boolean { - return this._disabled; + searchInputPaddingRight = `${defaultSearchInputPadding}px`; + + private _disabled: boolean = false; + public get disabled(): boolean { + return this._disabled; + } + @Input("disabled") + public set disabled(value: boolean) { + this._disabled = value; + + if (value) { + this.searchQuery?.disable({ emitEvent: false }); + } else { + this.searchQuery?.enable({ emitEvent: false }); } - @Input('disabled') - public set disabled(value: boolean) { - this._disabled = value; - - if (value) { - this.searchQuery?.disable({ emitEvent: false }); - } else { - this.searchQuery?.enable({ emitEvent: false }); + } + + @Input() searchQueryValue: string = ""; + @Input() clearSearchTitle: string = "Clear Search"; + @Input("placeholder") + set placeholder(pass: string) { + this.finalPlaceholder = pass ? pass : "Search"; + } + + @Input() helperText: string = ""; + @Input() debounceTime: number = 100; + @Input() searchTermMinimalLength: number = 2; + @Input() showSearchButton = false; + @Input() searchButtonText: string = "Search"; + @Input() searchButtonAriaLabelText: string = "Search"; + @Input() searchAriaLabelText: string; + + @Output() search: EventEmitter = new EventEmitter(); + + @ViewChild("searchButton", { read: ElementRef }) + searchButton?: ElementRef; + isSeachButtonVisible: boolean; + + private triggerSearch$: Subject = new BehaviorSubject(undefined); + private hasSearchBeenTriggeredManually: boolean; + + public searchQuery: FormControl; + public searchQuerySub: Subscription; + public focused: boolean = false; + public finalPlaceholder: string = "Search"; + + ngOnInit() { + this.searchQuery = new FormControl(this.searchQueryValue); + + this.searchQuerySub = combineLatest([ + this.searchQuery.valueChanges, + this.triggerSearch$, + ]) + .pipe(debounceTime(this.debounceTime)) + .subscribe(([query]) => { + const queryLength = query.length; + query = query.trim(); + this.searchQueryValue = query; + + // not emit search event if it hasn't been inputted something different from whitespace + if (this.searchQueryValue.length === 0 && queryLength !== 0) { + return; } - } - - @Input() searchQueryValue: string = ''; - @Input() clearSearchTitle: string = 'Clear Search'; - @Input('placeholder') - set placeholder(pass: string) { - this.finalPlaceholder = pass ? pass : 'Search'; - } - - @Input() helperText: string = ''; - @Input() debounceTime: number = 100; - @Input() searchTermMinimalLength: number = 2; - @Input() showSearchButton = false; - @Input() searchButtonText: string = 'Search'; - @Input() searchButtonAriaLabelText: string = 'Search'; - @Input() searchAriaLabelText: string; - - @Output() search: EventEmitter = new EventEmitter(); - - @ViewChild('searchButton', { read: ElementRef }) - searchButton?: ElementRef; - isSeachButtonVisible: boolean; - - private triggerSearch$: Subject = new BehaviorSubject(undefined); - private hasSearchBeenTriggeredManually: boolean; - - public searchQuery: FormControl; - public searchQuerySub: Subscription; - public focused: boolean = false; - public finalPlaceholder: string = 'Search'; - - ngOnInit() { - this.searchQuery = new FormControl(this.searchQueryValue); - - this.searchQuerySub = combineLatest([this.searchQuery.valueChanges, this.triggerSearch$]) - .pipe(debounceTime(this.debounceTime)) - .subscribe(([query]) => { - const queryLength = query.length; - query = query.trim(); - this.searchQueryValue = query; - - // not emit search event if it hasn't been inputted something different from whitespace - if (this.searchQueryValue.length === 0 && queryLength !== 0) { - return; - } - - // Make sure that the 'Search' button will be visible in 'Manual Search' mode upon every change. - this.isSeachButtonVisible = this.showSearchButton; - - const shouldNotifyForQueryChange = !this.showSearchButton || this.hasSearchBeenTriggeredManually; - const inputHasMinLengthOrIsCleared = - this.searchQueryValue.length >= this.searchTermMinimalLength || this.searchQueryValue.length === 0; - if (shouldNotifyForQueryChange && inputHasMinLengthOrIsCleared) { - // If we are about to notify that the search term has changed replace 'Search' button with the `X` one. - this.isSeachButtonVisible = false; - this.search.emit(query); - } - - this.hasSearchBeenTriggeredManually = false; - this.computeSearchInputPadding(); - }); - } - ngOnDestroy(): void { - if (this.searchQuerySub) { - this.searchQuerySub.unsubscribe(); + // Make sure that the 'Search' button will be visible in 'Manual Search' mode upon every change. + this.isSeachButtonVisible = this.showSearchButton; + + const shouldNotifyForQueryChange = + !this.showSearchButton || this.hasSearchBeenTriggeredManually; + const inputHasMinLengthOrIsCleared = + this.searchQueryValue.length >= this.searchTermMinimalLength || + this.searchQueryValue.length === 0; + if (shouldNotifyForQueryChange && inputHasMinLengthOrIsCleared) { + // If we are about to notify that the search term has changed replace 'Search' button with the `X` one. + this.isSeachButtonVisible = false; + this.search.emit(query); } - } - clearSearch(): void { - this.searchQuery.setValue(''); - if (this.showSearchButton) { - this.triggerSearch(); - } - } + this.hasSearchBeenTriggeredManually = false; + this.computeSearchInputPadding(); + }); + } - handleKeyDown(event: KeyboardEvent): void { - if (event.key === 'Enter') { - this.triggerSearch(); - } + ngOnDestroy(): void { + if (this.searchQuerySub) { + this.searchQuerySub.unsubscribe(); } + } - triggerSearch(): void { - this.hasSearchBeenTriggeredManually = true; - this.triggerSearch$.next(); + clearSearch(): void { + this.searchQuery.setValue(""); + if (this.showSearchButton) { + this.triggerSearch(); } + } - private computeSearchInputPadding(): void { - if (this.showSearchButton && this.isSeachButtonVisible) { - // Wait for the search button to be rendered as changes 'shouldShowSearchButton' might not be applied in the template. - // Useful especially after the first rendering. - setTimeout(() => { - this.searchInputPaddingRight = Math.round(this.searchButton?.nativeElement.clientWidth || defaultSearchInputPadding) + 'px'; - }); - } else { - this.searchInputPaddingRight = `${defaultSearchInputPadding}px`; - } + handleKeyDown(event: KeyboardEvent): void { + if (event.key === "Enter") { + this.triggerSearch(); + } + } + + triggerSearch(): void { + this.hasSearchBeenTriggeredManually = true; + this.triggerSearch$.next(); + } + + private computeSearchInputPadding(): void { + if (this.showSearchButton && this.isSeachButtonVisible) { + // Wait for the search button to be rendered as changes 'shouldShowSearchButton' might not be applied in the template. + // Useful especially after the first rendering. + setTimeout(() => { + this.searchInputPaddingRight = + Math.round( + this.searchButton?.nativeElement.clientWidth || + defaultSearchInputPadding, + ) + "px"; + }); + } else { + this.searchInputPaddingRight = `${defaultSearchInputPadding}px`; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts index e63ae17364..a95100273a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; -import { CommonModule } from '@angular/common'; -import { VdkSearchComponent } from './search.component'; -import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from "@angular/common"; +import { VdkSearchComponent } from "./search.component"; +import { ReactiveFormsModule } from "@angular/forms"; @NgModule({ - declarations: [VdkSearchComponent], - imports: [CommonModule, ReactiveFormsModule], - exports: [VdkSearchComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + declarations: [VdkSearchComponent], + imports: [CommonModule, ReactiveFormsModule], + exports: [VdkSearchComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class VdkSearchModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts index 97f12d2178..016d4dbdd3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts @@ -5,12 +5,15 @@ /* eslint-disable */ -import { Type } from '@angular/core'; -import { VdkToastComponent } from './toast.component'; -import { VdkToastContainerComponent } from './toast-container.component'; +import { Type } from "@angular/core"; +import { VdkToastComponent } from "./toast.component"; +import { VdkToastContainerComponent } from "./toast-container.component"; -export * from './toast.component'; -export * from './toast-container.component'; -export * from './toast.model'; +export * from "./toast.component"; +export * from "./toast-container.component"; +export * from "./toast.model"; -export const TOAST_DIRECTIVES: Type[] = [VdkToastContainerComponent, VdkToastComponent]; +export const TOAST_DIRECTIVES: Type[] = [ + VdkToastContainerComponent, + VdkToastComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html index afe84b4ed6..d877825ffa 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html @@ -4,5 +4,5 @@ -->
    - +
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss index a967f61811..cc8f37df85 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss @@ -3,30 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import '../../css/screen-sizes.scss'; +@import "../../css/screen-sizes.scss"; :host { - display: block; - z-index: 1040; - position: absolute; - right: 0; - width: 414px; - opacity: 1; - margin-top: 18px; + display: block; + z-index: 1040; + position: absolute; + right: 0; + width: 414px; + opacity: 1; + margin-top: 18px; - @media screen and(max-width: $xs-width) { - max-width: $min-el-size; - right: -12px; - } + @media screen and(max-width: $xs-width) { + max-width: $min-el-size; + right: -12px; + } } .toast-container { - margin-right: 0; - > div { - margin-left: 0px; - margin-right: -12px; - } - display: flex; - justify-content: flex-end; - flex-direction: column; + margin-right: 0; + > div { + margin-left: 0px; + margin-right: -12px; + } + display: flex; + justify-content: flex-end; + flex-direction: column; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts index 17d2af1dc3..0e619ca64f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts @@ -5,29 +5,43 @@ /* eslint-disable */ -import { HostBinding, Input, Component } from '@angular/core'; -import { trigger, transition, query, animateChild, stagger } from '@angular/animations'; +import { HostBinding, Input, Component } from "@angular/core"; +import { + trigger, + transition, + query, + animateChild, + stagger, +} from "@angular/animations"; -import { multiply, STAGGER_DURATION } from '../animation-constants'; +import { multiply, STAGGER_DURATION } from "../animation-constants"; -import { VdkToastComponent } from './toast.component'; +import { VdkToastComponent } from "./toast.component"; @Component({ - selector: 'vdk-toast-container', - templateUrl: './toast-container.component.html', - styleUrls: ['./toast-container.component.scss'], - animations: [ - trigger('toastContainer', [ - transition(':enter', [query('@launchToast', [stagger(`${multiply(STAGGER_DURATION)}ms`, animateChild())], { optional: true })]), - transition(':leave', [query('@launchToast', [animateChild()], { optional: true })]) - ]) - ] + selector: "vdk-toast-container", + templateUrl: "./toast-container.component.html", + styleUrls: ["./toast-container.component.scss"], + animations: [ + trigger("toastContainer", [ + transition(":enter", [ + query( + "@launchToast", + [stagger(`${multiply(STAGGER_DURATION)}ms`, animateChild())], + { optional: true }, + ), + ]), + transition(":leave", [ + query("@launchToast", [animateChild()], { optional: true }), + ]), + ]), + ], }) export class VdkToastContainerComponent { - @Input() topOffset: number = 0; + @Input() topOffset: number = 0; - @HostBinding('style.top') - get top(): string { - return 60 + this.topOffset + 'px'; - } + @HostBinding("style.top") + get top(): string { + return 60 + this.topOffset + "px"; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html index f3ea4e2a67..79b50ad9c7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html @@ -4,163 +4,126 @@ -->
    - + -
    - - - - - - - - +
    + + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - -
    + + + + + + + + +
    + +
    -
    -
    -
    - - +
    +
    +
    + + - -
    -
    + +
    +
    - - + + -
    - - -
    +
    + +
    +
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss index e5ceb9f600..be918aa30d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss @@ -3,294 +3,313 @@ * SPDX-License-Identifier: Apache-2.0 */ -@import '../../css/colors'; -@import '../../css/screen-sizes.scss'; +@import "../../css/colors"; +@import "../../css/screen-sizes.scss"; :host { - @media screen and(max-width: $xs-width) { - max-width: 75vw; - } - - ::ng-deep .toast-description { - font-size: 1em; - color: #adbbc4; - margin-top: 3px; - line-height: 18px; - - -webkit-line-clamp: 4; - -moz-line-clamp: 4; - display: -webkit-box; - overflow: hidden; - text-overflow: ellipsis; - // https://github.com/postcss/autoprefixer/issues/776 - /* autoprefixer: ignore next */ - -webkit-box-orient: vertical; - max-height: 70px; - } - - ::ng-deep .toast-link { - font-size: 0.85em; - color: #89cbdf; - display: block; - text-transform: uppercase; - font-family: 'Metropolis-Semibold'; - margin-top: 12px; - float: right; - letter-spacing: 1px; - } + @media screen and(max-width: $xs-width) { + max-width: 75vw; + } + + ::ng-deep .toast-description { + font-size: 1em; + color: #adbbc4; + margin-top: 3px; + line-height: 18px; + + -webkit-line-clamp: 4; + -moz-line-clamp: 4; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + // https://github.com/postcss/autoprefixer/issues/776 + /* autoprefixer: ignore next */ + -webkit-box-orient: vertical; + max-height: 70px; + } + + ::ng-deep .toast-link { + font-size: 0.85em; + color: #89cbdf; + display: block; + text-transform: uppercase; + font-family: "Metropolis-Semibold"; + margin-top: 12px; + float: right; + letter-spacing: 1px; + } - ::ng-deep .toast-date { - color: #adbbc4; - font-size: 11px; - margin-left: 6px; - position: relative; - top: 1px; - } + ::ng-deep .toast-date { + color: #adbbc4; + font-size: 11px; + margin-left: 6px; + position: relative; + top: 1px; + } - ::ng-deep .toast-title { - font-weight: 500; - color: #e9ecef; - font-size: 14px; - margin-top: 0; - line-height: 24px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - max-width: 240px; - } + ::ng-deep .toast-title { + font-weight: 500; + color: #e9ecef; + font-size: 14px; + margin-top: 0; + line-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 240px; + } } .toast { - display: flex; - flex-direction: row; - max-width: 100%; - min-width: $min-el-size; - background-color: $bgcolor; - border-radius: 3px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); - position: relative; - right: 18px; - padding: 12px; - margin: 0 0 12px 0; - - // animation - steady state of component - transform: scale(1, 1); - transform-origin: 100% 0; + display: flex; + flex-direction: row; + max-width: 100%; + min-width: $min-el-size; + background-color: $bgcolor; + border-radius: 3px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); + position: relative; + right: 18px; + padding: 12px; + margin: 0 0 12px 0; + + // animation - steady state of component + transform: scale(1, 1); + transform-origin: 100% 0; } .dismiss-bg { - .dismiss { - cds-icon[shape='times'] { - fill: #adbbc4; - margin-top: -8px; - margin-left: 1px; - } - } - background: transparent; - width: 30px; - height: 30px; - display: inline-block; - position: absolute; - right: 8px; - top: 8px; - cursor: pointer; - border-radius: 100%; - border: none; - - &:hover { - background-color: rgba(0, 0, 0, 0.15); + .dismiss { + cds-icon[shape="times"] { + fill: #adbbc4; + margin-top: -8px; + margin-left: 1px; } + } + background: transparent; + width: 30px; + height: 30px; + display: inline-block; + position: absolute; + right: 8px; + top: 8px; + cursor: pointer; + border-radius: 100%; + border: none; + + &:hover { + background-color: rgba(0, 0, 0, 0.15); + } } .dismiss { - position: absolute; - left: 6px; - top: 6px; + position: absolute; + left: 6px; + top: 6px; } .gradient { - width: 35%; - height: 100%; - background: linear-gradient(-90deg, rgba($bgcolor, 0) -14.71%, rgba($bgcolor, 1) 46.71%); - display: inline-block; - position: absolute; - top: 0px; - transform: scale(0, 1); - transform-origin: 0 100%; + width: 35%; + height: 100%; + background: linear-gradient( + -90deg, + rgba($bgcolor, 0) -14.71%, + rgba($bgcolor, 1) 46.71% + ); + display: inline-block; + position: absolute; + top: 0px; + transform: scale(0, 1); + transform-origin: 0 100%; } .toast-button { - font-weight: 500; - color: #89cbdf; - text-transform: uppercase; - font-size: 11px; - letter-spacing: 1px; - margin-top: 12px; + font-weight: 500; + color: #89cbdf; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 1px; + margin-top: 12px; } .toast-button.secondary { - margin-right: 6px; + margin-right: 6px; } button.toast-button { - background: none; - border: none; + background: none; + border: none; } .toast-title-container { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } div.clr-row { - padding-bottom: 10px; + padding-bottom: 10px; } div.content { - padding-left: 12px; - width: 100%; + padding-left: 12px; + width: 100%; } div.content-wrapper { - flex: 1; - display: flex; + flex: 1; + display: flex; } div.button-container { - text-align: right; + text-align: right; - button { - cursor: pointer; + button { + cursor: pointer; - &:hover { - color: $darkButtonHoverBlue; - } + &:hover { + color: $darkButtonHoverBlue; } + } } // icons .icon { - transform: scale(1); - transform-origin: center; + transform: scale(1); + transform-origin: center; } .icon-container { - margin: -6px; + margin: -6px; } // info #info-icon-outline, #info-icon-line { - fill: none; - stroke: #0095d3; - stroke-miterlimit: 10; - stroke-width: 2; + fill: none; + stroke: #0095d3; + stroke-miterlimit: 10; + stroke-width: 2; } #info-icon-line { - stroke-linecap: round; - stroke-dasharray: 16; - stroke-dashoffset: 0; - animation-fill-mode: forwards; + stroke-linecap: round; + stroke-dasharray: 16; + stroke-dashoffset: 0; + animation-fill-mode: forwards; } #info-icon-dot { - transform-origin: 50% 42%; - fill: #0095d3; + transform-origin: 50% 42%; + fill: #0095d3; } //warning #warn-icon-line, #warn-icon-triangle, #warn-icon-dot { - fill: #ffef5ff1; + fill: #ffef5ff1; } #warn-icon-dot { - transform-origin: 50% 54%; + transform-origin: 50% 54%; } // error #error-icon-outline, #error-icon-line { - fill: none; - stroke: #f54f47; - stroke-miterlimit: 10; - stroke-width: 2; + fill: none; + stroke: #f54f47; + stroke-miterlimit: 10; + stroke-width: 2; } #error-icon-line { - stroke-linecap: round; - stroke-width: 2.65; - stroke-dasharray: 7; - animation-fill-mode: forwards; - stroke-dashoffset: 0; + stroke-linecap: round; + stroke-width: 2.65; + stroke-dasharray: 7; + animation-fill-mode: forwards; + stroke-dashoffset: 0; } #error-icon-dot { - transform-origin: 50% 54%; - fill: #f54f47; + transform-origin: 50% 54%; + fill: #f54f47; } // success icon .checkmark { - fill: none; - stroke: #ffffff; - stroke-linecap: round; - stroke-miterlimit: 10; - stroke-width: 1.9px; - stroke-dasharray: 31.386688232421875; - animation-fill-mode: forwards; + fill: none; + stroke: #ffffff; + stroke-linecap: round; + stroke-miterlimit: 10; + stroke-width: 1.9px; + stroke-dasharray: 31.386688232421875; + animation-fill-mode: forwards; } :host-context(.dark) { - .toast { - background-color: $bgcolorDark; - } + .toast { + background-color: $bgcolorDark; + } - .gradient { - background: linear-gradient(-90deg, rgba($bgcolorDark, 0) -14.71%, rgba($bgcolorDark, 1) 46.71%); - } + .gradient { + background: linear-gradient( + -90deg, + rgba($bgcolorDark, 0) -14.71%, + rgba($bgcolorDark, 1) 46.71% + ); + } - ::ng-deep .toast-description { - color: #a9b6be; - } + ::ng-deep .toast-description { + color: #a9b6be; + } - ::ng-deep .toast-button { - color: #49afd9; + ::ng-deep .toast-button { + color: #49afd9; - &:hover { - color: #0095d3; - } + &:hover { + color: #0095d3; } + } - ::ng-deep .toast-date { - color: #a9b6be; - } + ::ng-deep .toast-date { + color: #a9b6be; + } - .dismiss-bg:hover { - background-color: rgba(255, 255, 255, 0.1); - } + .dismiss-bg:hover { + background-color: rgba(255, 255, 255, 0.1); + } } // MUTED styles :host-context(.muted) { - ::ng-deep .toast-title, - ::ng-deep .toast-date, - ::ng-deep .toast-description, - ::ng-deep .button-container button { - color: $inactiveGray; - } - - .info-bg, - .error-bg { - fill: $inactiveGray; - } - - .info-dot { - fill: $bgcolor; - } - + ::ng-deep .toast-title, + ::ng-deep .toast-date, + ::ng-deep .toast-description, + ::ng-deep .button-container button { + color: $inactiveGray; + } + + .info-bg, + .error-bg { + fill: $inactiveGray; + } + + .info-dot { + fill: $bgcolor; + } + + .info-bg, + .info-dot, + .info-line-vertical, + .info-line-horizontal, + .error-bg, + .error-line, + .error-dot { + stroke: $bgcolor; + } + + :host-context(.dark) { .info-bg, .info-dot, .info-line-vertical, @@ -298,22 +317,11 @@ div.button-container { .error-bg, .error-line, .error-dot { - stroke: $bgcolor; + stroke: $bgcolorDark; } - :host-context(.dark) { - .info-bg, - .info-dot, - .info-line-vertical, - .info-line-horizontal, - .error-bg, - .error-line, - .error-dot { - stroke: $bgcolorDark; - } - - .info-dot { - fill: $bgcolorDark; - } + .info-dot { + fill: $bgcolorDark; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts index 75322dd53f..9ecc985d3a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts @@ -5,313 +5,346 @@ /* eslint-disable */ -import { ElementRef, NgZone, Component, Input, Output, EventEmitter, HostListener, Optional } from '@angular/core'; -import { trigger, group, style, animate, transition, query, animateChild, keyframes } from '@angular/animations'; +import { + ElementRef, + NgZone, + Component, + Input, + Output, + EventEmitter, + HostListener, + Optional, +} from "@angular/core"; +import { + trigger, + group, + style, + animate, + transition, + query, + animateChild, + keyframes, +} from "@angular/animations"; -import { timer } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { timer } from "rxjs"; +import { take } from "rxjs/operators"; -import { VmwToastType } from './toast.model'; -import { TRANSLATIONS } from './toast.l10n'; -import { VdkSimpleTranslateService } from '../../ngx-utils'; +import { VmwToastType } from "./toast.model"; +import { TRANSLATIONS } from "./toast.l10n"; +import { VdkSimpleTranslateService } from "../../ngx-utils"; import { - multiply, - componentPrimaryEnterCurve, - componentPrimaryEnterTiming, - componentPrimaryLeaveCurve, - componentPrimaryLeaveTiming, - linePrimaryEnterCurve, - linePrimaryEnterTiming, - linePrimaryEnterDelay, - lineSecondaryEnterCurve, - lineSecondaryEnterTiming, - lineSecondaryEnterDelay, - DISMISS_ICON_DELAY, - DISMISS_ICON_DURATION, - DISMISS_ICON_CURVE, - GRADIENT_DURATION, - GRADIENT_DELAY, - GRADIENT_LEAVE_CURVE -} from '../animation-constants'; + multiply, + componentPrimaryEnterCurve, + componentPrimaryEnterTiming, + componentPrimaryLeaveCurve, + componentPrimaryLeaveTiming, + linePrimaryEnterCurve, + linePrimaryEnterTiming, + linePrimaryEnterDelay, + lineSecondaryEnterCurve, + lineSecondaryEnterTiming, + lineSecondaryEnterDelay, + DISMISS_ICON_DELAY, + DISMISS_ICON_DURATION, + DISMISS_ICON_CURVE, + GRADIENT_DURATION, + GRADIENT_DELAY, + GRADIENT_LEAVE_CURVE, +} from "../animation-constants"; const AUTODISMISS_TIMEOUT_SECONDS = 6; const TRACKED_TAG = { - A: true, - BUTTON: true + A: true, + BUTTON: true, }; @Component({ - selector: 'vdk-toast', - templateUrl: './toast.component.html', - styleUrls: ['./toast.component.scss'], - animations: [ - trigger('launchToast', [ - transition(':enter', [ - // toast parent element animation - group([ - style({ - transform: 'translateX(48px) scale(0, 1)' - }), - animate( - `${multiply(componentPrimaryEnterTiming)}ms ${componentPrimaryEnterCurve}`, - style({ - transform: 'translateX(0) scale(1, 1)' - }) - ), - - // use optional: true for if/else elements - query('.checkmark', animateChild(), { optional: true }), - query('#info-icon-dot', animateChild(), { optional: true }), - query('#info-icon-line', animateChild(), { optional: true }), - query('#warn-icon-dot', animateChild(), { optional: true }), - query('#warn-icon-line', animateChild(), { optional: true }), - query('#error-icon-dot', animateChild(), { optional: true }), - query('#error-icon-line', animateChild(), { optional: true }), - query('.gradient', animateChild()), - query('.dismiss', animateChild(), { optional: true }) - ]) - ]), - - // START LEAVE ANIMATION - // ':leave' is a default state for ngIf and ngFor, doesn't need to be predefined - transition( - ':leave', - [ - group([ - style({ - transform: 'translateX(0px) scale(1, 1)', - marginTop: '*' - }), - - // use query self to be able to group the animation on the current element - query(':self', [ - animate( - `${multiply(componentPrimaryLeaveTiming)}ms ${componentPrimaryLeaveCurve}`, - style({ - transform: 'translateX(18px) scale(0, 1)' - }) - ), + selector: "vdk-toast", + templateUrl: "./toast.component.html", + styleUrls: ["./toast.component.scss"], + animations: [ + trigger("launchToast", [ + transition(":enter", [ + // toast parent element animation + group([ + style({ + transform: "translateX(48px) scale(0, 1)", + }), + animate( + `${multiply(componentPrimaryEnterTiming)}ms ${componentPrimaryEnterCurve}`, + style({ + transform: "translateX(0) scale(1, 1)", + }), + ), - animate( - `${multiply(componentPrimaryLeaveTiming)}ms ${componentPrimaryLeaveCurve}`, - style({ - marginTop: '-{{height}}px' - }) - ) - ]), - - query('.toast-description, .toast-title, .icon, .button-container, .dismiss-bg, .dismiss, .toast-date', [ - animate( - `${multiply(10)}ms`, - style({ - opacity: '0' - }) - ) - ]) - ]) - ], - { - params: { - height: 0 - } - } - ) - // end launchToast + // use optional: true for if/else elements + query(".checkmark", animateChild(), { optional: true }), + query("#info-icon-dot", animateChild(), { optional: true }), + query("#info-icon-line", animateChild(), { optional: true }), + query("#warn-icon-dot", animateChild(), { optional: true }), + query("#warn-icon-line", animateChild(), { optional: true }), + query("#error-icon-dot", animateChild(), { optional: true }), + query("#error-icon-line", animateChild(), { optional: true }), + query(".gradient", animateChild()), + query(".dismiss", animateChild(), { optional: true }), ]), + ]), - // info icon animation - trigger('infoLine', [ - transition('* => *', [ - animate( - `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, - keyframes([style({ strokeDashoffset: '16', offset: 0 }), style({ strokeDashoffset: '0', offset: 1.0 })]) - ) - ]) - ]), - trigger('infoDot', [ - transition('* => *', [ - style({ - transform: 'scale(0)' - }), - animate( - `${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, - style({ - transform: 'scale(1)' - }) - ) - ]) - ]), + // START LEAVE ANIMATION + // ':leave' is a default state for ngIf and ngFor, doesn't need to be predefined + transition( + ":leave", + [ + group([ + style({ + transform: "translateX(0px) scale(1, 1)", + marginTop: "*", + }), - // error icon animation - trigger('errorLine', [ - transition('* => *', [ - animate( - `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, - keyframes([style({ strokeDashoffset: '7.919999599456787', offset: 0 }), style({ strokeDashoffset: '0', offset: 1.0 })]) - ) - ]) - ]), - trigger('errorDot', [ - transition('* => *', [ + // use query self to be able to group the animation on the current element + query(":self", [ + animate( + `${multiply(componentPrimaryLeaveTiming)}ms ${componentPrimaryLeaveCurve}`, style({ - transform: 'scale(0)' + transform: "translateX(18px) scale(0, 1)", }), - animate( - `${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, - style({ - transform: 'scale(1)' - }) - ) - ]) - ]), + ), - //warning icon animation - trigger('warnLine', [ - transition('* => *', [ - animate( - `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, - keyframes([style({ strokeDashoffset: '7.919999599456787', offset: 0 }), style({ strokeDashoffset: '0', offset: 1.0 })]) - ) - ]) - ]), - trigger('warnDot', [ - transition('* => *', [ + animate( + `${multiply(componentPrimaryLeaveTiming)}ms ${componentPrimaryLeaveCurve}`, style({ - transform: 'scale(0)' + marginTop: "-{{height}}px", }), - animate( - `${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, - style({ - transform: 'scale(1)' - }) - ) - ]) - ]), + ), + ]), - // success icon animation - trigger('checkmarkLine', [ - transition('* => *', [ - // css keyframe animation + query( + ".toast-description, .toast-title, .icon, .button-container, .dismiss-bg, .dismiss, .toast-date", + [ animate( - `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, - keyframes([style({ strokeDashoffset: '31.386688232421875', offset: 0 }), style({ strokeDashoffset: '0', offset: 1.0 })]) - ) - ]) - ]), + `${multiply(10)}ms`, + style({ + opacity: "0", + }), + ), + ], + ), + ]), + ], + { + params: { + height: 0, + }, + }, + ), + // end launchToast + ]), - // moving the gradient offview - trigger('gradientMove', [ - transition('* => *', [ - style({ - transform: 'scale(1, 1)' - }), - animate( - `${multiply(GRADIENT_DURATION)}ms ${multiply(GRADIENT_DELAY)}ms ${GRADIENT_LEAVE_CURVE}`, - style({ - transform: 'scale(0, 1)' - }) - ) - ]) - ]), + // info icon animation + trigger("infoLine", [ + transition("* => *", [ + animate( + `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, + keyframes([ + style({ strokeDashoffset: "16", offset: 0 }), + style({ strokeDashoffset: "0", offset: 1.0 }), + ]), + ), + ]), + ]), + trigger("infoDot", [ + transition("* => *", [ + style({ + transform: "scale(0)", + }), + animate( + `${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, + style({ + transform: "scale(1)", + }), + ), + ]), + ]), - // fade in the dismiss icon - trigger('dismissIconVisible', [ - transition('* => *', [ - style({ - opacity: '0' - }), - animate( - `${multiply(DISMISS_ICON_DURATION)}ms ${multiply(DISMISS_ICON_DELAY)}ms ${DISMISS_ICON_CURVE}`, - style({ - opacity: '1' - }) - ) - ]) - ]) - ] + // error icon animation + trigger("errorLine", [ + transition("* => *", [ + animate( + `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, + keyframes([ + style({ strokeDashoffset: "7.919999599456787", offset: 0 }), + style({ strokeDashoffset: "0", offset: 1.0 }), + ]), + ), + ]), + ]), + trigger("errorDot", [ + transition("* => *", [ + style({ + transform: "scale(0)", + }), + animate( + `${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, + style({ + transform: "scale(1)", + }), + ), + ]), + ]), + + //warning icon animation + trigger("warnLine", [ + transition("* => *", [ + animate( + `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, + keyframes([ + style({ strokeDashoffset: "7.919999599456787", offset: 0 }), + style({ strokeDashoffset: "0", offset: 1.0 }), + ]), + ), + ]), + ]), + trigger("warnDot", [ + transition("* => *", [ + style({ + transform: "scale(0)", + }), + animate( + `${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, + style({ + transform: "scale(1)", + }), + ), + ]), + ]), + + // success icon animation + trigger("checkmarkLine", [ + transition("* => *", [ + // css keyframe animation + animate( + `${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, + keyframes([ + style({ strokeDashoffset: "31.386688232421875", offset: 0 }), + style({ strokeDashoffset: "0", offset: 1.0 }), + ]), + ), + ]), + ]), + + // moving the gradient offview + trigger("gradientMove", [ + transition("* => *", [ + style({ + transform: "scale(1, 1)", + }), + animate( + `${multiply(GRADIENT_DURATION)}ms ${multiply(GRADIENT_DELAY)}ms ${GRADIENT_LEAVE_CURVE}`, + style({ + transform: "scale(0, 1)", + }), + ), + ]), + ]), + + // fade in the dismiss icon + trigger("dismissIconVisible", [ + transition("* => *", [ + style({ + opacity: "0", + }), + animate( + `${multiply(DISMISS_ICON_DURATION)}ms ${multiply(DISMISS_ICON_DELAY)}ms ${DISMISS_ICON_CURVE}`, + style({ + opacity: "1", + }), + ), + ]), + ]), + ], }) export class VdkToastComponent { - public mouseover = false; - public focused = false; + public mouseover = false; + public focused = false; - @Input() type: VmwToastType = VmwToastType.INFO; - @Input() primaryButtonText: string; - @Input() secondaryButtonText: string; - @Input() dismissible: boolean = true; - @Input() timeoutSeconds: number = AUTODISMISS_TIMEOUT_SECONDS; + @Input() type: VmwToastType = VmwToastType.INFO; + @Input() primaryButtonText: string; + @Input() secondaryButtonText: string; + @Input() dismissible: boolean = true; + @Input() timeoutSeconds: number = AUTODISMISS_TIMEOUT_SECONDS; - @Output() dismissed = new EventEmitter(); - @Output() primaryButtonClick = new EventEmitter(); - @Output() secondaryButtonClick = new EventEmitter(); + @Output() dismissed = new EventEmitter(); + @Output() primaryButtonClick = new EventEmitter(); + @Output() secondaryButtonClick = new EventEmitter(); - readonly VmwToastType = VmwToastType; - disableAutoDismiss: boolean = false; - height: number; - animate = true; + readonly VmwToastType = VmwToastType; + disableAutoDismiss: boolean = false; + height: number; + animate = true; - constructor( - private element: ElementRef, - private ngZone: NgZone, - public translateService: VdkSimpleTranslateService // @Optional() private segmentService, - ) { - this.translateService.loadTranslationsForComponent('toast', TRANSLATIONS); - } + constructor( + private element: ElementRef, + private ngZone: NgZone, + public translateService: VdkSimpleTranslateService, // @Optional() private segmentService, + ) { + this.translateService.loadTranslationsForComponent("toast", TRANSLATIONS); + } - ngOnInit() { - this.setUpTimer(); - } + ngOnInit() { + this.setUpTimer(); + } - @HostListener('click', ['$event']) - trackClicks(event: any) { - return; - } + @HostListener("click", ["$event"]) + trackClicks(event: any) { + return; + } - mouseOver(over: boolean) { - // If the user moves their mouse over the snack, disable auto-dismiss - this.disableAutoDismiss = over; - } + mouseOver(over: boolean) { + // If the user moves their mouse over the snack, disable auto-dismiss + this.disableAutoDismiss = over; + } - focus(focused: boolean) { - this.disableAutoDismiss = focused; - } + focus(focused: boolean) { + this.disableAutoDismiss = focused; + } - get loaded() { - return { - value: this.animate, - params: { - height: this.element.nativeElement.clientHeight - } - }; - } + get loaded() { + return { + value: this.animate, + params: { + height: this.element.nativeElement.clientHeight, + }, + }; + } - dismiss(userDismissed: boolean = false) { - this.animate = false; + dismiss(userDismissed: boolean = false) { + this.animate = false; - // before we tell the app to remove the toast, give the leave animation - // some time to run... - timer(multiply(componentPrimaryLeaveTiming + 200)) - .pipe(take(1)) - .subscribe(() => { - this.dismissed.emit(); - }); - } + // before we tell the app to remove the toast, give the leave animation + // some time to run... + timer(multiply(componentPrimaryLeaveTiming + 200)) + .pipe(take(1)) + .subscribe(() => { + this.dismissed.emit(); + }); + } - private setUpTimer() { - if (this.timeoutSeconds > 0) { - this.ngZone.runOutsideAngular(() => { - timer(this.timeoutSeconds * multiply(1000)) - .pipe(take(1)) - .subscribe(() => { - this.ngZone.run(() => { - if (this.disableAutoDismiss) { - this.setUpTimer(); - return; - } - this.dismiss(); - }); - }); + private setUpTimer() { + if (this.timeoutSeconds > 0) { + this.ngZone.runOutsideAngular(() => { + timer(this.timeoutSeconds * multiply(1000)) + .pipe(take(1)) + .subscribe(() => { + this.ngZone.run(() => { + if (this.disableAutoDismiss) { + this.setUpTimer(); + return; + } + this.dismiss(); }); - } + }); + }); } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts index 69ff1c7c5f..cb322dd33f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts @@ -6,76 +6,76 @@ /* eslint-disable */ export const TRANSLATIONS = { - en: { - 'dismiss-notification': 'Dismiss notification', - 'success-icon': 'Success Icon', - 'info-icon': 'Information Icon', - 'failure-icon': 'Failure Icon' - }, - es: { - 'dismiss-notification': 'Descartar notificación', - 'success-icon': 'Icono de éxito', - 'failure-icon': 'Icono de error', - 'info-icon': 'Icono de información' - }, - de: { - 'dismiss-notification': 'Benachrichtigung verwerfen', - 'success-icon': 'Symbol „Erfolgreich“', - 'failure-icon': 'Fehlersymbol', - 'info-icon': 'Informationssymbol' - }, - fr: { - 'dismiss-notification': 'Ignorer la notification', - 'success-icon': 'Icône de réussite', - 'failure-icon': "Icône d'échec", - 'info-icon': "Icône d'informations" - }, - ja: { - 'dismiss-notification': '通知を破棄', - 'success-icon': '成功アイコン', - 'failure-icon': '障害アイコン', - 'info-icon': '情報アイコン' - }, - ko: { - 'dismiss-notification': '알림 해제', - 'success-icon': '성공 아이콘', - 'failure-icon': '실패 아이콘', - 'info-icon': '정보 아이콘' - }, - zh_TW: { - 'dismiss-notification': '關閉通知', - 'success-icon': '成功图标', - 'failure-icon': '失敗圖示', - 'info-icon': '資訊圖示' - }, - zh_CN: { - 'dismiss-notification': '关闭通知', - 'success-icon': '成功图标', - 'info-icon': '信息图标', - 'failure-icon': '故障图标' - }, - it: { - 'dismiss-notification': 'Ignora notifica', - 'success-icon': 'Icona Operazione riuscita', - 'info-icon': 'Icona Informazioni', - 'failure-icon': 'Icona Errore' - }, - nl: { - 'dismiss-notification': 'Melding negeren', - 'success-icon': 'Pictogram Geslaagd', - 'info-icon': 'Pictogram Informatie', - 'failure-icon': 'Pictogram Fout' - }, - pt: { - 'dismiss-notification': 'Descartar notificação', - 'success-icon': 'Ícone de êxito', - 'info-icon': 'Ícone de informação', - 'failure-icon': 'Ícone de falha' - }, - ru: { - 'dismiss-notification': 'Закрыть уведомление', - 'success-icon': 'Значок успешного выполнения', - 'info-icon': 'Значок информации', - 'failure-icon': 'Значок ошибки' - } + en: { + "dismiss-notification": "Dismiss notification", + "success-icon": "Success Icon", + "info-icon": "Information Icon", + "failure-icon": "Failure Icon", + }, + es: { + "dismiss-notification": "Descartar notificación", + "success-icon": "Icono de éxito", + "failure-icon": "Icono de error", + "info-icon": "Icono de información", + }, + de: { + "dismiss-notification": "Benachrichtigung verwerfen", + "success-icon": "Symbol „Erfolgreich“", + "failure-icon": "Fehlersymbol", + "info-icon": "Informationssymbol", + }, + fr: { + "dismiss-notification": "Ignorer la notification", + "success-icon": "Icône de réussite", + "failure-icon": "Icône d'échec", + "info-icon": "Icône d'informations", + }, + ja: { + "dismiss-notification": "通知を破棄", + "success-icon": "成功アイコン", + "failure-icon": "障害アイコン", + "info-icon": "情報アイコン", + }, + ko: { + "dismiss-notification": "알림 해제", + "success-icon": "성공 아이콘", + "failure-icon": "실패 아이콘", + "info-icon": "정보 아이콘", + }, + zh_TW: { + "dismiss-notification": "關閉通知", + "success-icon": "成功图标", + "failure-icon": "失敗圖示", + "info-icon": "資訊圖示", + }, + zh_CN: { + "dismiss-notification": "关闭通知", + "success-icon": "成功图标", + "info-icon": "信息图标", + "failure-icon": "故障图标", + }, + it: { + "dismiss-notification": "Ignora notifica", + "success-icon": "Icona Operazione riuscita", + "info-icon": "Icona Informazioni", + "failure-icon": "Icona Errore", + }, + nl: { + "dismiss-notification": "Melding negeren", + "success-icon": "Pictogram Geslaagd", + "info-icon": "Pictogram Informatie", + "failure-icon": "Pictogram Fout", + }, + pt: { + "dismiss-notification": "Descartar notificação", + "success-icon": "Ícone de êxito", + "info-icon": "Ícone de informação", + "failure-icon": "Ícone de falha", + }, + ru: { + "dismiss-notification": "Закрыть уведомление", + "success-icon": "Значок успешного выполнения", + "info-icon": "Значок информации", + "failure-icon": "Значок ошибки", + }, }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts index 1c0dcc0a45..cb84f18b11 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts @@ -4,8 +4,8 @@ */ export enum VmwToastType { - SUCCESS = 'success', - FAILURE = 'failure', - INFO = 'info', - WARN = 'warning' + SUCCESS = "success", + FAILURE = "failure", + INFO = "info", + WARN = "warning", } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts index 71333b465b..0e5dc586a4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts @@ -3,84 +3,99 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA, ModuleWithProviders, NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + CUSTOM_ELEMENTS_SCHEMA, + ModuleWithProviders, + NgModule, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { ClarityModule, ClrDropdownModule, ClrLoadingButtonModule, ClrLoadingModule, ClrTooltipModule } from '@clr/angular'; +import { + ClarityModule, + ClrDropdownModule, + ClrLoadingButtonModule, + ClrLoadingModule, + ClrTooltipModule, +} from "@clr/angular"; -import { VdkSimpleTranslateModule } from '../ngx-utils'; +import { VdkSimpleTranslateModule } from "../ngx-utils"; import { - angleIcon, - arrowIcon, - checkCircleIcon, - checkIcon, - ClarityIcons, - copyToClipboardIcon, - exclamationCircleIcon, - searchIcon, - timesCircleIcon, - timesIcon -} from '@cds/core/icon'; + angleIcon, + arrowIcon, + checkCircleIcon, + checkIcon, + ClarityIcons, + copyToClipboardIcon, + exclamationCircleIcon, + searchIcon, + timesCircleIcon, + timesIcon, +} from "@cds/core/icon"; -import { VdkEmptyStatePlaceholderModule } from './empty-state-placeholder/empty-state-placeholder.module'; +import { VdkEmptyStatePlaceholderModule } from "./empty-state-placeholder/empty-state-placeholder.module"; -import { COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES } from './copy-to-clipboard-button/index'; -import { FORM_SECTION_DIRECTIVES } from './form-section/index'; -import { FORM_SECTION_CONTAINER_DIRECTIVES } from './form-section-container/index'; -import { TOAST_DIRECTIVES } from './toast/index'; -import { VdkSearchModule } from './search'; +import { COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES } from "./copy-to-clipboard-button/index"; +import { FORM_SECTION_DIRECTIVES } from "./form-section/index"; +import { FORM_SECTION_CONTAINER_DIRECTIVES } from "./form-section-container/index"; +import { TOAST_DIRECTIVES } from "./toast/index"; +import { VdkSearchModule } from "./search"; @NgModule({ - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - ClarityModule, - ClrTooltipModule, - ClrDropdownModule, - ClrLoadingModule, - ClrLoadingButtonModule, - VdkSimpleTranslateModule, - VdkEmptyStatePlaceholderModule, - VdkSearchModule - ], - declarations: [COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES, FORM_SECTION_DIRECTIVES, FORM_SECTION_CONTAINER_DIRECTIVES, TOAST_DIRECTIVES], - exports: [ - COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES, - FORM_SECTION_DIRECTIVES, - FORM_SECTION_CONTAINER_DIRECTIVES, - TOAST_DIRECTIVES, - VdkEmptyStatePlaceholderModule, - VdkSearchModule - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + ClarityModule, + ClrTooltipModule, + ClrDropdownModule, + ClrLoadingModule, + ClrLoadingButtonModule, + VdkSimpleTranslateModule, + VdkEmptyStatePlaceholderModule, + VdkSearchModule, + ], + declarations: [ + COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES, + FORM_SECTION_DIRECTIVES, + FORM_SECTION_CONTAINER_DIRECTIVES, + TOAST_DIRECTIVES, + ], + exports: [ + COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES, + FORM_SECTION_DIRECTIVES, + FORM_SECTION_CONTAINER_DIRECTIVES, + TOAST_DIRECTIVES, + VdkEmptyStatePlaceholderModule, + VdkSearchModule, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class VdkSharedComponentsModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: VdkSharedComponentsModule - }; - } + static forRoot(): ModuleWithProviders { + return { + ngModule: VdkSharedComponentsModule, + }; + } - static forChild(): ModuleWithProviders { - return { - ngModule: VdkSharedComponentsModule - }; - } + static forChild(): ModuleWithProviders { + return { + ngModule: VdkSharedComponentsModule, + }; + } - constructor() { - ClarityIcons.addIcons( - angleIcon, - arrowIcon, - checkCircleIcon, - checkIcon, - copyToClipboardIcon, - exclamationCircleIcon, - searchIcon, - timesCircleIcon, - timesIcon - ); - } + constructor() { + ClarityIcons.addIcons( + angleIcon, + arrowIcon, + checkCircleIcon, + checkIcon, + copyToClipboardIcon, + exclamationCircleIcon, + searchIcon, + timesCircleIcon, + timesIcon, + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts index ebf773cfbc..eb0755aff4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './simple-translate-service/index'; +export * from "./simple-translate-service/index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts index 137be22a3b..c983c97c2a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './simple-translate.module'; -export * from './simple-translate.service'; -export * from './simple-translate.pipe'; +export * from "./simple-translate.module"; +export * from "./simple-translate.service"; +export * from "./simple-translate.pipe"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts index bba2d7bbec..dc1af15bde 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts @@ -3,20 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule, ModuleWithProviders } from '@angular/core'; +import { NgModule, ModuleWithProviders } from "@angular/core"; -import { VdkSimpleTranslateService } from './simple-translate.service'; -import { VdkSimpleTranslatePipe } from './simple-translate.pipe'; +import { VdkSimpleTranslateService } from "./simple-translate.service"; +import { VdkSimpleTranslatePipe } from "./simple-translate.pipe"; @NgModule({ - declarations: [VdkSimpleTranslatePipe], - exports: [VdkSimpleTranslatePipe] + declarations: [VdkSimpleTranslatePipe], + exports: [VdkSimpleTranslatePipe], }) export class VdkSimpleTranslateModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: VdkSimpleTranslateModule, - providers: [VdkSimpleTranslateService] - }; - } + static forRoot(): ModuleWithProviders { + return { + ngModule: VdkSimpleTranslateModule, + providers: [VdkSimpleTranslateService], + }; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts index b8c97d936b..beb6f8fc30 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts @@ -5,18 +5,18 @@ /* eslint-disable */ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; -import { VdkSimpleTranslateService } from './simple-translate.service'; +import { VdkSimpleTranslateService } from "./simple-translate.service"; @Pipe({ - name: 'simpleTranslate', - pure: false + name: "simpleTranslate", + pure: false, }) export class VdkSimpleTranslatePipe implements PipeTransform { - constructor(private simpleTranslate: VdkSimpleTranslateService) {} + constructor(private simpleTranslate: VdkSimpleTranslateService) {} - transform(text: string, ...args: any[]) { - return this.simpleTranslate.translate(text, ...args); - } + transform(text: string, ...args: any[]) { + return this.simpleTranslate.translate(text, ...args); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts index f1ba51bbe5..26c51d8312 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts @@ -5,66 +5,70 @@ /* eslint-disable */ -import { TestBed, inject } from '@angular/core/testing'; +import { TestBed, inject } from "@angular/core/testing"; -import { VdkSimpleTranslateService } from './simple-translate.service'; +import { VdkSimpleTranslateService } from "./simple-translate.service"; const mockTranslations = { - en: { - 'chat-with-vmware-support': 'Chat with VMware Support', - 'return-to-chat': 'Return to chat' - }, - es: { - 'chat-with-vmware-support': 'Conversa con VMware Support', - 'return-to-chat': 'Retornar a conversacion' - } + en: { + "chat-with-vmware-support": "Chat with VMware Support", + "return-to-chat": "Return to chat", + }, + es: { + "chat-with-vmware-support": "Conversa con VMware Support", + "return-to-chat": "Retornar a conversacion", + }, }; -describe('VdkSimpleTranslateService', () => { - let service: VdkSimpleTranslateService; +describe("VdkSimpleTranslateService", () => { + let service: VdkSimpleTranslateService; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [VdkSimpleTranslateService] - }); - - service = TestBed.inject(VdkSimpleTranslateService); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [VdkSimpleTranslateService], }); - it('should accept a LCID and set language', () => { - expect(service).toBeTruthy(); - expect(service.getLanguage()).toEqual('en'); - service.setLanguage('es-CO'); - expect(service.getLanguage()).toEqual('es'); - service.setLanguage('es_CO'); - expect(service.getLanguage()).toEqual('es'); - service.setLanguage('zh_TW'); - expect(service.getLanguage()).toEqual('zh_TW'); - service.setLanguage('zh'); - expect(service.getLanguage()).toEqual('zh'); - }); + service = TestBed.inject(VdkSimpleTranslateService); + }); - it('should translate string as per specified language', () => { - service.setLanguage('es-CO'); + it("should accept a LCID and set language", () => { + expect(service).toBeTruthy(); + expect(service.getLanguage()).toEqual("en"); + service.setLanguage("es-CO"); + expect(service.getLanguage()).toEqual("es"); + service.setLanguage("es_CO"); + expect(service.getLanguage()).toEqual("es"); + service.setLanguage("zh_TW"); + expect(service.getLanguage()).toEqual("zh_TW"); + service.setLanguage("zh"); + expect(service.getLanguage()).toEqual("zh"); + }); - service.loadTranslationsForComponent('test', mockTranslations); + it("should translate string as per specified language", () => { + service.setLanguage("es-CO"); - expect(service.translate('test.return-to-chat')).toEqual('Retornar a conversacion'); - }); + service.loadTranslationsForComponent("test", mockTranslations); - it('should return translation in the default langauge if the string does not exist in the specified language', () => { - service.setLanguage('pl-PL'); + expect(service.translate("test.return-to-chat")).toEqual( + "Retornar a conversacion", + ); + }); - service.loadTranslationsForComponent('test', mockTranslations); + it("should return translation in the default langauge if the string does not exist in the specified language", () => { + service.setLanguage("pl-PL"); - expect(service.translate('test.return-to-chat')).toEqual('Return to chat'); - }); + service.loadTranslationsForComponent("test", mockTranslations); - it('error message should show if key is not found', () => { - service.setLanguage('pl-PL'); + expect(service.translate("test.return-to-chat")).toEqual("Return to chat"); + }); - service.loadTranslationsForComponent('test', mockTranslations); + it("error message should show if key is not found", () => { + service.setLanguage("pl-PL"); - expect(service.translate('test.some-non-existing-key')).toEqual('!! Key test.some-non-existing-key not found !!'); - }); + service.loadTranslationsForComponent("test", mockTranslations); + + expect(service.translate("test.some-non-existing-key")).toEqual( + "!! Key test.some-non-existing-key not found !!", + ); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts index b2334871d3..8b517c2146 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts @@ -5,91 +5,94 @@ /* eslint-disable */ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; -const DEFAULT_LANGUAGE = 'en'; +const DEFAULT_LANGUAGE = "en"; @Injectable({ - providedIn: 'root' + providedIn: "root", }) export class VdkSimpleTranslateService { - private language: string = DEFAULT_LANGUAGE; - private translations: any = {}; - - constructor() { - const w: any = window as any; - const language = w.navigator.language || w.navigator.userLanguage; - this.setLanguage(language); - } - - loadTranslationsForComponent(componentKey: string, translationsToAdd: any) { - for (let language in translationsToAdd) { - if (!this.translations[language]) { - this.translations[language] = {}; - } - - for (let y in translationsToAdd[language]) { - let newKey = componentKey + '.' + y; - this.translations[language][newKey] = translationsToAdd[language][y]; - } - } - } - - setLanguage(language: string) { - language = language.replace('-', '_'); - - // check if different locale names are used for Simplified or Traditional Chinese - const chineseMapping = { - zh_Hans: 'zh_CN', - zh_Hant: 'zh_TW' - }; - language = chineseMapping[language] || language; - - // Special-case Chinese because we support both TW and CN locales - // Also if the locale is pt_PT(Portuguese Portugal) we should default to EN - // otherwise the 2 character language ID is what we want - const mainLocale = language.substring(0, 2); - const isChinese = language.length > 2 && mainLocale === 'zh'; - const isNotSupportedPortuguese = language.length > 2 && mainLocale === 'pt' && language !== 'pt_BR'; - - if (isChinese || isNotSupportedPortuguese) { - this.language = language; - } else { - this.language = mainLocale; - } + private language: string = DEFAULT_LANGUAGE; + private translations: any = {}; + + constructor() { + const w: any = window as any; + const language = w.navigator.language || w.navigator.userLanguage; + this.setLanguage(language); + } + + loadTranslationsForComponent(componentKey: string, translationsToAdd: any) { + for (let language in translationsToAdd) { + if (!this.translations[language]) { + this.translations[language] = {}; + } + + for (let y in translationsToAdd[language]) { + let newKey = componentKey + "." + y; + this.translations[language][newKey] = translationsToAdd[language][y]; + } } - - getLanguage() { - return this.language; + } + + setLanguage(language: string) { + language = language.replace("-", "_"); + + // check if different locale names are used for Simplified or Traditional Chinese + const chineseMapping = { + zh_Hans: "zh_CN", + zh_Hant: "zh_TW", + }; + language = chineseMapping[language] || language; + + // Special-case Chinese because we support both TW and CN locales + // Also if the locale is pt_PT(Portuguese Portugal) we should default to EN + // otherwise the 2 character language ID is what we want + const mainLocale = language.substring(0, 2); + const isChinese = language.length > 2 && mainLocale === "zh"; + const isNotSupportedPortuguese = + language.length > 2 && mainLocale === "pt" && language !== "pt_BR"; + + if (isChinese || isNotSupportedPortuguese) { + this.language = language; + } else { + this.language = mainLocale; } + } - translate(key: any, ...args: any[]) { - let translations = this.translations[this.language]; - - //if there are no translations of the user's language set translations to equal the default language - if (!translations) { - translations = this.translations[DEFAULT_LANGUAGE] ? this.translations[DEFAULT_LANGUAGE] : []; - } + getLanguage() { + return this.language; + } - //if there is a translation get that translation otherwise use the default language - let translation = undefined; - if (translations[key]) { - translation = translations[key]; - } else if (this.translations[DEFAULT_LANGUAGE]) { - translation = this.translations[DEFAULT_LANGUAGE][key]; - } + translate(key: any, ...args: any[]) { + let translations = this.translations[this.language]; - //if no translation is found in the user's language or in the default language then show an error string - if (!translation) { - translation = `!! Key ${key} not found !!`; - } + //if there are no translations of the user's language set translations to equal the default language + if (!translations) { + translations = this.translations[DEFAULT_LANGUAGE] + ? this.translations[DEFAULT_LANGUAGE] + : []; + } - return this.format(translation, ...args); + //if there is a translation get that translation otherwise use the default language + let translation = undefined; + if (translations[key]) { + translation = translations[key]; + } else if (this.translations[DEFAULT_LANGUAGE]) { + translation = this.translations[DEFAULT_LANGUAGE][key]; } - private format(format: string, ...args: string[]) { - return format.replace(/{(\d+)}/g, function (match, number) { - return typeof args[number] != 'undefined' ? args[number] : match; - }); + //if no translation is found in the user's language or in the default language then show an error string + if (!translation) { + translation = `!! Key ${key} not found !!`; } + + return this.format(translation, ...args); + } + + private format(format: string, ...args: string[]) { + return format.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] != "undefined" ? args[number] : match; + }); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts index 9aa0d08a4c..34a8caf07c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts @@ -7,4 +7,4 @@ * Public API Surface of commons */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/index.ts index 5bf0ae39c7..cceafc2358 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './taurus-error-base.component'; +export * from "./taurus-error-base.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.spec.ts index c92a7487f7..a013a3d87b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.spec.ts @@ -3,114 +3,138 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { generateErrorCodes } from '../../../../unit-testing'; +import { generateErrorCodes } from "../../../../unit-testing"; -import { ErrorCodes, ErrorRecord } from '../../../../common'; +import { ErrorCodes, ErrorRecord } from "../../../../common"; -import { TaurusErrorBaseComponent } from './taurus-error-base.component'; +import { TaurusErrorBaseComponent } from "./taurus-error-base.component"; @Component({ - selector: 'shared-taurus-error-base-subclass-component', - template: '' + selector: "shared-taurus-error-base-subclass-component", + template: "", }) // eslint-disable-next-line @angular-eslint/component-class-suffix class TaurusErrorBaseComponentStub extends TaurusErrorBaseComponent { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'TaurusErrorBaseComponentStub'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Taurus-Error-Base-Component-Stub'; - - constructor() { - super(TaurusErrorBaseComponentStub.CLASS_NAME); - } + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "TaurusErrorBaseComponentStub"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = + "Taurus-Error-Base-Component-Stub"; + + constructor() { + super(TaurusErrorBaseComponentStub.CLASS_NAME); + } } -describe('TaurusErrorBaseComponent', () => { - let randomServiceStub: jasmine.SpyObj<{ load: () => void; errorCodes: ErrorCodes<{ load: () => void }> }>; +describe("TaurusErrorBaseComponent", () => { + let randomServiceStub: jasmine.SpyObj<{ + load: () => void; + errorCodes: ErrorCodes<{ load: () => void }>; + }>; - let fixture: ComponentFixture; - let component: TaurusErrorBaseComponentStub; + let fixture: ComponentFixture; + let component: TaurusErrorBaseComponentStub; - beforeEach(() => { - randomServiceStub = jasmine.createSpyObj<{ load: () => void; errorCodes: ErrorCodes<{ load: () => void }> }>('randomServiceStub', [ - 'load' - ]); + beforeEach(() => { + randomServiceStub = jasmine.createSpyObj<{ + load: () => void; + errorCodes: ErrorCodes<{ load: () => void }>; + }>("randomServiceStub", ["load"]); - TestBed.configureTestingModule({ - declarations: [TaurusErrorBaseComponentStub], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); - - fixture = TestBed.createComponent(TaurusErrorBaseComponentStub); - component = fixture.componentInstance; + TestBed.configureTestingModule({ + declarations: [TaurusErrorBaseComponentStub], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }); - it('should verify component is created', () => { + fixture = TestBed.createComponent(TaurusErrorBaseComponentStub); + component = fixture.componentInstance; + }); + + it("should verify component is created", () => { + // When + const component1 = new TaurusErrorBaseComponent(); + + // Then + expect(component).toBeDefined(); + expect(component).toBeInstanceOf(TaurusErrorBaseComponentStub); + expect(component).toBeInstanceOf(TaurusErrorBaseComponent); + expect(component1).toBeDefined(); + expect(component1).toBeInstanceOf(TaurusErrorBaseComponent); + }); + + describe("Angular lifecycle hooks::", () => { + describe("|ngOnDestroy|", () => { + it("should verify will dispose error records in error store", () => { + // Given + const spyErrorStoreDispose = spyOn( + component.errors, + "dispose", + ).and.callThrough(); + // When - const component1 = new TaurusErrorBaseComponent(); + fixture.destroy(); // Then - expect(component).toBeDefined(); - expect(component).toBeInstanceOf(TaurusErrorBaseComponentStub); - expect(component).toBeInstanceOf(TaurusErrorBaseComponent); - expect(component1).toBeDefined(); - expect(component1).toBeInstanceOf(TaurusErrorBaseComponent); + expect(spyErrorStoreDispose).toHaveBeenCalled(); + }); }); + }); - describe('Angular lifecycle hooks::', () => { - describe('|ngOnDestroy|', () => { - it('should verify will dispose error records in error store', () => { - // Given - const spyErrorStoreDispose = spyOn(component.errors, 'dispose').and.callThrough(); - - // When - fixture.destroy(); + describe("Methods::", () => { + describe("|generateErrorCode|", () => { + it("should verify will return error code", () => { + // When + // @ts-ignore + const errorCode = component.generateErrorCode( + "ClassName", + "Public-Name", + "methodName", + "unknown", + ); - // Then - expect(spyErrorStoreDispose).toHaveBeenCalled(); - }); - }); + // Then + expect(errorCode).toEqual("ClassName_Public-Name_methodName_unknown"); + }); }); - describe('Methods::', () => { - describe('|generateErrorCode|', () => { - it('should verify will return error code', () => { - // When - // @ts-ignore - const errorCode = component.generateErrorCode('ClassName', 'Public-Name', 'methodName', 'unknown'); - - // Then - expect(errorCode).toEqual('ClassName_Public-Name_methodName_unknown'); - }); - }); - - describe('|processServiceRequestError|', () => { - it('should verify will process error and record in local store', () => { - // Given - const error = new Error('Random Error'); - const spyErrorRecord = spyOn(component.errors, 'record').and.callThrough(); - - generateErrorCodes(randomServiceStub, ['load'], 'ClassName', 'Public-Name'); - - // When - // @ts-ignore - component.processServiceRequestError(randomServiceStub.errorCodes.load, error); - - // Then - expect(spyErrorRecord).toHaveBeenCalledWith({ - code: randomServiceStub.errorCodes.load.Unknown, - objectUUID: component.objectUUID, - error - } as ErrorRecord); - }); - }); + describe("|processServiceRequestError|", () => { + it("should verify will process error and record in local store", () => { + // Given + const error = new Error("Random Error"); + const spyErrorRecord = spyOn( + component.errors, + "record", + ).and.callThrough(); + + generateErrorCodes( + randomServiceStub, + ["load"], + "ClassName", + "Public-Name", + ); + + // When + // @ts-ignore + component.processServiceRequestError( + randomServiceStub.errorCodes.load, + error, + ); + + // Then + expect(spyErrorRecord).toHaveBeenCalledWith({ + code: randomServiceStub.errorCodes.load.Unknown, + objectUUID: component.objectUUID, + error, + } as ErrorRecord); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.ts index 329f25bcef..fc4be352bb 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/error-base/taurus-error-base.component.ts @@ -3,11 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive, OnDestroy } from '@angular/core'; +import { Directive, OnDestroy } from "@angular/core"; -import { ErrorStore, generateErrorCode, ServiceHttpErrorCodes, TaurusObject } from '../../../../common'; +import { + ErrorStore, + generateErrorCode, + ServiceHttpErrorCodes, + TaurusObject, +} from "../../../../common"; -import { ErrorStoreImpl, processServiceRequestError } from '../../../error'; +import { ErrorStoreImpl, processServiceRequestError } from "../../../error"; /** * ** Taurus base component with provided in some way auto handling for errors. @@ -17,71 +22,89 @@ import { ErrorStoreImpl, processServiceRequestError } from '../../../error'; * while in TaurusBaseComponent it's handled in the upper level inside the Effects and provided through ComponentModel to the Component itself. */ @Directive() -export class TaurusErrorBaseComponent extends TaurusObject implements OnDestroy { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'TaurusErrorBaseComponent'; +export class TaurusErrorBaseComponent + extends TaurusObject + implements OnDestroy +{ + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "TaurusErrorBaseComponent"; - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Taurus-Error-Base-Component'; + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Taurus-Error-Base-Component"; - /** - * ** Reference to local ErrorStore. - */ - errors: ErrorStore; + /** + * ** Reference to local ErrorStore. + */ + errors: ErrorStore; - /** - * ** Error codes supported in class and its corresponding subclasses. - */ - errorCodes: Readonly>; + /** + * ** Error codes supported in class and its corresponding subclasses. + */ + errorCodes: Readonly>; - /** - * ** Constructor. - */ - constructor(className: string = null) { - super(className ?? TaurusErrorBaseComponent.CLASS_NAME); + /** + * ** Constructor. + */ + constructor(className: string = null) { + super(className ?? TaurusErrorBaseComponent.CLASS_NAME); - this.errors = ErrorStoreImpl.empty(); - this.errorCodes = {}; - } + this.errors = ErrorStoreImpl.empty(); + this.errorCodes = {}; + } - /** - * @inheritDoc - */ - override ngOnDestroy(): void { - this.errors.dispose(); + /** + * @inheritDoc + */ + override ngOnDestroy(): void { + this.errors.dispose(); - super.ngOnDestroy(); - } + super.ngOnDestroy(); + } - /** - * ** Generates Error code (token). - * - * - Code (token) should start with Class name, - * then followed by underscore and class PUBLIC_NAME, - * then followed by underscore and method name or underscore with some error specifics, - * and followed by underscore and additional details to avoid overlaps with other Class errors. - * - *
    - * returned value pattern: - *

    - * ___ - *

    - */ - protected generateErrorCode(className: string, classPublicName: string, methodName: string, additionalDetails?: string): string { - return generateErrorCode(className, classPublicName, methodName, additionalDetails); - } + /** + * ** Generates Error code (token). + * + * - Code (token) should start with Class name, + * then followed by underscore and class PUBLIC_NAME, + * then followed by underscore and method name or underscore with some error specifics, + * and followed by underscore and additional details to avoid overlaps with other Class errors. + * + *
    + * returned value pattern: + *

    + * ___ + *

    + */ + protected generateErrorCode( + className: string, + classPublicName: string, + methodName: string, + additionalDetails?: string, + ): string { + return generateErrorCode( + className, + classPublicName, + methodName, + additionalDetails, + ); + } - /** - * ** Process service HTTP request error and return ErrorRecord. - * - * @param {Record} serviceHttpErrorCodes - is map of Service method supported error codes auto-handling - * @param {unknown} error - is actual error object reference - */ - protected processServiceRequestError(serviceHttpErrorCodes: Record, error: unknown): void { - this.errors.record(processServiceRequestError(this.objectUUID, serviceHttpErrorCodes, error)); - } + /** + * ** Process service HTTP request error and return ErrorRecord. + * + * @param {Record} serviceHttpErrorCodes - is map of Service method supported error codes auto-handling + * @param {unknown} error - is actual error object reference + */ + protected processServiceRequestError( + serviceHttpErrorCodes: Record, + error: unknown, + ): void { + this.errors.record( + processServiceRequestError(this.objectUUID, serviceHttpErrorCodes, error), + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/index.ts index 69925e954b..873c16903c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './error-base'; -export * from './redux-base'; +export * from "./error-base"; +export * from "./redux-base"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/index.ts index 78e4e17a7e..d5e9e1da0e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './interfaces'; -export * from './taurus-base.component'; +export * from "./interfaces"; +export * from "./taurus-base.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/index.ts index 71447b2583..c0703173b9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './taurus-component-lifecycle-hooks.interface'; +export * from "./taurus-component-lifecycle-hooks.interface"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/taurus-component-lifecycle-hooks.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/taurus-component-lifecycle-hooks.interface.ts index 8a9efa4afc..897591e5c8 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/taurus-component-lifecycle-hooks.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/interfaces/taurus-component-lifecycle-hooks.interface.ts @@ -3,22 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ErrorRecord } from '../../../../../common'; +import { ErrorRecord } from "../../../../../common"; -import { ComponentModel } from '../../../model'; +import { ComponentModel } from "../../../model"; /** * ** Taurus Component Lifecycle hook for Model initialized. */ export interface OnTaurusModelInit { - /** - * ** Fires once per Route, after Component is initialized and - * immediately after {@link ComponentModel} is initialized and it is bound to the Component model field. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - */ - onModelInit(model?: ComponentModel, task?: string): void; + /** + * ** Fires once per Route, after Component is initialized and + * immediately after {@link ComponentModel} is initialized and it is bound to the Component model field. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelInit(model?: ComponentModel, task?: string): void; } /** @@ -27,64 +27,64 @@ export interface OnTaurusModelInit { * ** Taurus Component Lifecycle hook for Model First Load. */ export interface OnTaurusModelFirstLoad { - /** - * ** Fires when something in State change and its status is LOADED or FAILED, and it fires only once. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - * - *

    - * - General hook ideal for Ui state restore, or something that need Read->Action->Done behaviour. - *

    - */ - onModelFirstLoad(model?: ComponentModel, task?: string): void; + /** + * ** Fires when something in State change and its status is LOADED or FAILED, and it fires only once. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * + *

    + * - General hook ideal for Ui state restore, or something that need Read->Action->Done behaviour. + *

    + */ + onModelFirstLoad(model?: ComponentModel, task?: string): void; } /** * ** Taurus Component Lifecycle hook for Model Initial Load. */ export interface OnTaurusModelInitialLoad { - /** - * ** Fires when something in State change and its status is LOADED or FAILED, and it fires only once. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - * - *

    - * - General hook ideal for Ui state restore, or something that need Read->Action->Done behaviour. - *

    - */ - onModelInitialLoad(model?: ComponentModel, task?: string): void; + /** + * ** Fires when something in State change and its status is LOADED or FAILED, and it fires only once. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * + *

    + * - General hook ideal for Ui state restore, or something that need Read->Action->Done behaviour. + *

    + */ + onModelInitialLoad(model?: ComponentModel, task?: string): void; } /** * ** Taurus Component Lifecycle hook for Model Loaded. */ export interface OnTaurusModelLoad { - /** - * ** Fires when something in State change and its status is LOADED or FAILED. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - * - *

    - * - General hook ideal for loading spinner HIDE. - *

    - */ - onModelLoad(model?: ComponentModel, task?: string): void; + /** + * ** Fires when something in State change and its status is LOADED or FAILED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * + *

    + * - General hook ideal for loading spinner HIDE. + *

    + */ + onModelLoad(model?: ComponentModel, task?: string): void; } /** * ** Taurus Component Lifecycle hook for Model Changed. */ export interface OnTaurusModelChange { - /** - * ** Fires when something in State change and its status is LOADED. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - */ - onModelChange(model?: ComponentModel, task?: string): void; + /** + * ** Fires when something in State change and its status is LOADED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelChange(model?: ComponentModel, task?: string): void; } /** @@ -93,33 +93,37 @@ export interface OnTaurusModelChange { * ** Taurus Component Lifecycle hook for Model Failed. */ export interface OnTaurusModelFail { - /** - * ** Fires when something in State change and its status is FAILED. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - */ - onModelFail(model?: ComponentModel, task?: string): void; + /** + * ** Fires when something in State change and its status is FAILED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelFail(model?: ComponentModel, task?: string): void; } /** * ** Taurus Component Lifecycle hook for Model Failed. */ export interface OnTaurusModelError { - /** - * ** Fires when something in State change and its status is FAILED. - * - * - model - ComponentModel optional parameter. - * - task - Is string optional parameter that inject context to callback for the specific operation. - * - newErrorRecords - Is Array of newly appeared ErrorRecords since previous hook execution, distinct against previous ComponentModel. - */ - onModelError(model?: ComponentModel, task?: string, newErrorRecords?: ErrorRecord[]): void; + /** + * ** Fires when something in State change and its status is FAILED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * - newErrorRecords - Is Array of newly appeared ErrorRecords since previous hook execution, distinct against previous ComponentModel. + */ + onModelError( + model?: ComponentModel, + task?: string, + newErrorRecords?: ErrorRecord[], + ): void; } export type TaurusComponentHooks = OnTaurusModelInit & - OnTaurusModelInitialLoad & - OnTaurusModelFirstLoad & - OnTaurusModelLoad & - OnTaurusModelChange & - OnTaurusModelError & - OnTaurusModelFail; + OnTaurusModelInitialLoad & + OnTaurusModelFirstLoad & + OnTaurusModelLoad & + OnTaurusModelChange & + OnTaurusModelError & + OnTaurusModelFail; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.spec.ts index 589d169fb5..627ef60963 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.spec.ts @@ -5,882 +5,1250 @@ /* eslint-disable @typescript-eslint/dot-notation,@typescript-eslint/no-unsafe-argument */ -import { Component, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + Component, + CUSTOM_ELEMENTS_SCHEMA, + OnDestroy, + OnInit, +} from "@angular/core"; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + Params, +} from "@angular/router"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { BehaviorSubject, of } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { BehaviorSubject, of } from "rxjs"; +import { take } from "rxjs/operators"; -import { CallFake } from '../../../../unit-testing'; +import { CallFake } from "../../../../unit-testing"; -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { ErrorRecord, TaurusObject } from '../../../../common'; +import { ErrorRecord, TaurusObject } from "../../../../common"; -import { RouteState } from '../../../router'; -import { RouteStateFactory } from '../../../router/factory'; +import { RouteState } from "../../../router"; +import { RouteStateFactory } from "../../../router/factory"; -import { NavigationService } from '../../../navigation'; +import { NavigationService } from "../../../navigation"; -import { ComponentModel, ComponentState, FAILED, LOADED } from '../../model'; -import { ComponentService } from '../../services'; +import { ComponentModel, ComponentState, FAILED, LOADED } from "../../model"; +import { ComponentService } from "../../services"; import { - OnTaurusModelChange, - OnTaurusModelError, - OnTaurusModelFail, - OnTaurusModelFirstLoad, - OnTaurusModelInit, - OnTaurusModelInitialLoad, - OnTaurusModelLoad -} from './interfaces'; + OnTaurusModelChange, + OnTaurusModelError, + OnTaurusModelFail, + OnTaurusModelFirstLoad, + OnTaurusModelInit, + OnTaurusModelInitialLoad, + OnTaurusModelLoad, +} from "./interfaces"; -import { TaurusBaseComponent } from './taurus-base.component'; +import { TaurusBaseComponent } from "./taurus-base.component"; const errorRecord1: ErrorRecord = { - code: 'ClassName_Public-Name_methodName_unknown', - error: new Error('Error 1'), - time: CollectionsUtil.dateNow(), - objectUUID: 'objectUUID' + code: "ClassName_Public-Name_methodName_unknown", + error: new Error("Error 1"), + time: CollectionsUtil.dateNow(), + objectUUID: "objectUUID", } as ErrorRecord; const errorRecord2: ErrorRecord = { - code: 'ClassName_Public-Name_methodName_unknown', - error: new Error('Error 2'), - time: CollectionsUtil.dateNow(), - objectUUID: 'objectUUID' + code: "ClassName_Public-Name_methodName_unknown", + error: new Error("Error 2"), + time: CollectionsUtil.dateNow(), + objectUUID: "objectUUID", } as ErrorRecord; const errorRecord3: ErrorRecord = { - code: 'ClassName_Public-Name_methodName_500', - error: new Error('Error 3'), - time: CollectionsUtil.dateNow(), - objectUUID: 'objectUUID' + code: "ClassName_Public-Name_methodName_500", + error: new Error("Error 3"), + time: CollectionsUtil.dateNow(), + objectUUID: "objectUUID", } as ErrorRecord; @Component({ - selector: 'shared-taurus-base-subclass-component', - template: '' + selector: "shared-taurus-base-subclass-component", + template: "", }) // eslint-disable-next-line @angular-eslint/component-class-suffix class TaurusBaseComponentStub - extends TaurusBaseComponent - implements OnInit, OnDestroy, OnTaurusModelInit, OnTaurusModelLoad, OnTaurusModelFirstLoad, OnTaurusModelChange, OnTaurusModelFail + extends TaurusBaseComponent + implements + OnInit, + OnDestroy, + OnTaurusModelInit, + OnTaurusModelLoad, + OnTaurusModelFirstLoad, + OnTaurusModelChange, + OnTaurusModelFail { - uuid = 'TaurusBaseComponentStub'; + uuid = "TaurusBaseComponentStub"; + + constructor( + componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute, + ) { + super(componentService, navigationService, activatedRoute); + } + + onModelInit(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelFirstLoad(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelLoad(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelChange(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelFail(_model?: ComponentModel, _task?: string) { + // No-op. + } + + override ngOnInit() { + super.ngOnInit(); + } + + override ngOnDestroy() { + super.ngOnDestroy(); + } +} - constructor(componentService: ComponentService, navigationService: NavigationService, activatedRoute: ActivatedRoute) { - super(componentService, navigationService, activatedRoute); - } +@Component({ + selector: "shared-taurus-base-subclass-component-v1", + template: "", +}) +// eslint-disable-next-line @angular-eslint/component-class-suffix +class TaurusBaseComponentStubV1 + extends TaurusBaseComponentStub + implements OnTaurusModelInitialLoad, OnTaurusModelError +{ + override uuid = "TaurusBaseComponentStubV1"; - onModelInit(_model?: ComponentModel, _task?: string) { - // No-op. - } + override enforceRouteReuse = true; - onModelFirstLoad(_model?: ComponentModel, _task?: string) { - // No-op. - } + onModelInitialLoad(_model?: ComponentModel, _task?: string): void { + // No-op. + } - onModelLoad(_model?: ComponentModel, _task?: string) { - // No-op. - } + onModelError( + _model?: ComponentModel, + _task?: string, + _newErrorRecords?: ErrorRecord[], + ): void { + // No-op. + } +} - onModelChange(_model?: ComponentModel, _task?: string) { - // No-op. - } +class ComponentModelStub extends ComponentModel { + task: string; + + randomId = 0; + + constructor(task?: string, randomId?: number) { + super(null, null); + + this.task = task; + this.randomId = randomId; + } + + get errors() { + return { + distinctErrorRecords: (_errorRecords: ErrorRecord[]) => { + if (this.randomId === 1) { + return []; + } + + if (this.randomId === 2) { + return [errorRecord1]; + } + + if (this.randomId === 3) { + return [errorRecord2, errorRecord3]; + } + + return []; + }, + records: [], + }; + } + + override get status() { + return null; + } + + override isModified(): boolean { + return null; + } + + override withStatusIdle() { + return this; + } + + override prepareForDestroy() { + return this; + } + + override getComponentState() { + return { + errors: this.errors, + } as ComponentState; + } + + override getTask() { + return this.task; + } + + override clearTask() { + return this; + } +} - onModelFail(_model?: ComponentModel, _task?: string) { - // No-op. - } +class ComponentModelStubV1 extends ComponentModelStub { + // @ts-ignore + override get previousModel(): Readonly { + return this._previousModel; + } - override ngOnInit() { - super.ngOnInit(); - } + // @ts-ignore + set previousModel(previousModel: Readonly) { + this._previousModel = previousModel; + } - override ngOnDestroy() { - super.ngOnDestroy(); - } + private _previousModel: Readonly; } -@Component({ - selector: 'shared-taurus-base-subclass-component-v1', - template: '' -}) -// eslint-disable-next-line @angular-eslint/component-class-suffix -class TaurusBaseComponentStubV1 extends TaurusBaseComponentStub implements OnTaurusModelInitialLoad, OnTaurusModelError { - override uuid = 'TaurusBaseComponentStubV1'; +describe("TaurusBaseComponent -> TaurusSubclassComponent", () => { + let componentServiceStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let activatedRouteStub: Partial; + let activatedRouteSnapshotStub: ActivatedRouteSnapshot; + let paramsSubject: BehaviorSubject; + + let routeStateStub: RouteState; + + let fixture: ComponentFixture; + let component: TaurusBaseComponentStub; + let modelStub: ComponentModel; + + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj( + "componentService", + ["init", "idle", "getModel", "update"], + ); + navigationServiceStub = jasmine.createSpyObj( + "navigationService", + ["navigateTo", "navigateBack"], + ); + activatedRouteSnapshotStub = { + url: "", + params: {}, + data: {}, + } as any; + paramsSubject = new BehaviorSubject({ name: "Ina", asset: 2 }); + activatedRouteStub = { + snapshot: activatedRouteSnapshotStub, + params: paramsSubject, + }; + routeStateStub = { + routePathSegments: ["path1/path2", "path3/path4"], + } as RouteState; + spyOn(RouteStateFactory.prototype, "create").and.returnValue( + routeStateStub, + ); + + modelStub = new ComponentModelStub() as any; + + TestBed.configureTestingModule({ + declarations: [TaurusBaseComponentStub, TaurusBaseComponentStubV1], + providers: [ + { provide: ComponentService, useValue: componentServiceStub }, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: ActivatedRoute, useValue: activatedRouteStub }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); - override enforceRouteReuse = true; + fixture = TestBed.createComponent(TaurusBaseComponentStub); + component = fixture.componentInstance; + }); - onModelInitialLoad(_model?: ComponentModel, _task?: string): void { - // No-op. - } + it("should verify component is created", () => { + // Given + component.model = modelStub; - onModelError(_model?: ComponentModel, _task?: string, _newErrorRecords?: ErrorRecord[]): void { - // No-op. - } -} + // Then + expect(component).toBeDefined(); + expect(component).toBeInstanceOf(TaurusBaseComponentStub); + expect(component).toBeInstanceOf(TaurusBaseComponent); + }); -class ComponentModelStub extends ComponentModel { - task: string; + describe("Properties::", () => { + describe("|uuid|", () => { + it("should verify default value is undefined", () => { + // Given + component.model = modelStub; - randomId = 0; + // When + expect(component.uuid).toEqual("TaurusBaseComponentStub"); + }); + }); - constructor(task?: string, randomId?: number) { - super(null, null); + describe("|componentService|", () => { + it("should verify ComponentService will be injected", () => { + // Given + component.model = modelStub; - this.task = task; - this.randomId = randomId; - } + // When + expect(component["componentService"]).toBeDefined(); + }); + }); - get errors() { - return { - distinctErrorRecords: (_errorRecords: ErrorRecord[]) => { - if (this.randomId === 1) { - return []; - } + describe("|navigationService|", () => { + it("should verify NavigationService will be injected", () => { + // Given + component.model = modelStub; - if (this.randomId === 2) { - return [errorRecord1]; - } + // When + expect(component["navigationService"]).toBeDefined(); + }); + }); - if (this.randomId === 3) { - return [errorRecord2, errorRecord3]; - } + describe("|activatedRoute|", () => { + it("should verify ActivatedRoute will be injected", () => { + // Given + component.model = modelStub; - return []; - }, - records: [] - }; - } + // When + expect(component["activatedRoute"]).toBeDefined(); + }); + }); + }); - override get status() { - return null; - } + describe("Angular lifecycle hooks::", () => { + beforeEach(() => { + component.model = modelStub; + }); - override isModified(): boolean { - return null; - } + describe("|ngOnInit|", () => { + it("should verify will invoke expected method", () => { + // Given + // @ts-ignore + const bindModelSpy = spyOn(component, "bindModel").and.callFake( + CallFake, + ); - override withStatusIdle() { - return this; - } + // When + fixture.detectChanges(); - override prepareForDestroy() { - return this; - } + // Then + expect(bindModelSpy).toHaveBeenCalled(); - override getComponentState() { - return { - errors: this.errors - } as ComponentState; - } + // Post + fixture.componentInstance.model = modelStub; + }); + }); - override getTask() { - return this.task; - } + describe("|ngOnDestroy|", () => { + it("should verify will invoke expected methods", () => { + // Given + spyOn(TaurusBaseComponent.prototype, "ngOnInit").and.callFake(CallFake); + // @ts-ignore + const setIdleSpy: jasmine.Spy<() => void> = spyOn( + TaurusBaseComponent.prototype, + "setStateIdle", + ).and.callFake(CallFake); + const ngOnDestroySpy: jasmine.Spy<() => void> = spyOn( + TaurusObject.prototype, + "ngOnDestroy", + ).and.callFake(CallFake); + + // When + fixture = TestBed.createComponent(TaurusBaseComponentStub); + fixture.componentInstance.model = modelStub; + fixture.detectChanges(); + fixture.destroy(); - override clearTask() { - return this; - } -} + // Then + expect(setIdleSpy).toHaveBeenCalled(); + expect(ngOnDestroySpy).toHaveBeenCalled(); + }); + }); + }); -class ComponentModelStubV1 extends ComponentModelStub { - // @ts-ignore - override get previousModel(): Readonly { - return this._previousModel; - } + describe("Methods::", () => { + describe("|navigateTo|", () => { + it("should verify will invoke correct methods", () => { + // Given + const replaceValues = { "$.team": "team1", "$.entity": "Prime" }; + navigationServiceStub.navigateTo.and.returnValue(Promise.resolve(true)); + component.model = modelStub; - // @ts-ignore - set previousModel(previousModel: Readonly) { - this._previousModel = previousModel; - } + // When + const promise = component.navigateTo(replaceValues); - private _previousModel: Readonly; -} + // Then + expect(promise).toBeInstanceOf(Promise); + expect(navigationServiceStub.navigateTo).toHaveBeenCalledWith( + replaceValues, + ); + }); + }); -describe('TaurusBaseComponent -> TaurusSubclassComponent', () => { - let componentServiceStub: jasmine.SpyObj; - let navigationServiceStub: jasmine.SpyObj; - let activatedRouteStub: Partial; - let activatedRouteSnapshotStub: ActivatedRouteSnapshot; - let paramsSubject: BehaviorSubject; + describe("|navigateBack|", () => { + it("should verify will invoke correct methods", () => { + // Given + const replaceValues = { "$.team": "team10", "$.entity": "Second" }; + navigationServiceStub.navigateBack.and.returnValue( + Promise.resolve(false), + ); + component.model = modelStub; - let routeStateStub: RouteState; + // When + const promise = component.navigateBack(replaceValues); - let fixture: ComponentFixture; - let component: TaurusBaseComponentStub; - let modelStub: ComponentModel; + // Then + expect(promise).toBeInstanceOf(Promise); + expect(navigationServiceStub.navigateBack).toHaveBeenCalledWith( + replaceValues, + ); + }); + }); - beforeEach(() => { - componentServiceStub = jasmine.createSpyObj('componentService', ['init', 'idle', 'getModel', 'update']); - navigationServiceStub = jasmine.createSpyObj('navigationService', ['navigateTo', 'navigateBack']); - activatedRouteSnapshotStub = { - url: '', - params: {}, - data: {} - } as any; - paramsSubject = new BehaviorSubject({ name: 'Ina', asset: 2 }); - activatedRouteStub = { snapshot: activatedRouteSnapshotStub, params: paramsSubject }; - routeStateStub = { - routePathSegments: ['path1/path2', 'path3/path4'] - } as RouteState; - spyOn(RouteStateFactory.prototype, 'create').and.returnValue(routeStateStub); + describe("|bindModel|", () => { + it("should verify will create correct subscriptions", () => { + // Given + componentServiceStub.init.and.returnValue(of(modelStub).pipe(take(1))); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(componentServiceStub.init).toHaveBeenCalledWith( + component.uuid, + routeStateStub, + ); + expect(componentServiceStub.getModel).toHaveBeenCalledWith( + component.uuid, + routeStateStub.routePathSegments, + ); + expect(component["subscriptions"].length).toEqual(1); // eslint-disable-line @typescript-eslint/no-unsafe-member-access + }); + + it("should verify will invoke correct Taurus lifecycle hooks for LOADED and modified", () => { + // Given + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it("should verify will invoke correct Taurus lifecycle hooks for LOADED and modified with Task", () => { + // Given + const task = "delete_entity"; + modelStub = new ComponentModelStub(task) as any; + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it("should verify will invoke correct Taurus lifecycle hooks for FAILED and modified deprecated", () => { + // Given + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(FAILED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelFailSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + }); + + it("should verify will invoke correct Taurus lifecycle hooks for FAILED and modified with Task deprecated", () => { + // Given + const task = "patch_entity"; + modelStub = new ComponentModelStub(task) as any; + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(FAILED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelFailSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + }); + + it("should verify will invoke correct Taurus lifecycle hooks for FAILED and modified", () => { + // Given + spyOn(TaurusBaseComponentStubV1.prototype, "onModelInit").and.callFake( + CallFake, + ); + spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelInitialLoad", + ).and.callFake(CallFake); + spyOn(TaurusBaseComponentStubV1.prototype, "onModelLoad").and.callFake( + CallFake, + ); + spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelErrorSpy = spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelError", + ).and.callFake(CallFake); + + spyOn(modelStub, "isModified").and.returnValue(true); + spyOnProperty(modelStub, "status", "get").and.returnValue(FAILED); + + const modelStub1: ComponentModel = new ComponentModelStub( + null, + 1, + ) as any; + spyOn(modelStub1, "isModified").and.returnValue(true); + spyOnProperty(modelStub1, "status", "get").and.returnValue(FAILED); + const modelStub2: ComponentModel = new ComponentModelStub( + null, + 2, + ) as any; + spyOn(modelStub2, "isModified").and.returnValue(true); + spyOnProperty(modelStub2, "status", "get").and.returnValue(FAILED); + const modelStub3: ComponentModel = new ComponentModelStub( + null, + 3, + ) as any; + spyOn(modelStub3, "isModified").and.returnValue(true); + spyOnProperty(modelStub3, "status", "get").and.returnValue(FAILED); + const modelStub4: ComponentModel = new ComponentModelStub( + null, + 4, + ) as any; + spyOn(modelStub4, "isModified").and.returnValue(true); + spyOnProperty(modelStub4, "status", "get").and.returnValue(FAILED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + + const modelSubject = new BehaviorSubject(modelStub1); + componentServiceStub.getModel.and.returnValue(modelSubject); + + const component1: TaurusBaseComponentStubV1 = TestBed.createComponent( + TaurusBaseComponentStubV1, + ).componentInstance; + + // When + // @ts-ignore + component1.bindModel(); + // @ts-ignore + component1.model = null; + modelSubject.next(modelStub2); + modelSubject.next(modelStub3); + modelSubject.next(modelStub4); - modelStub = new ComponentModelStub() as any; + // Then + expect(onModelErrorSpy.calls.argsFor(0)).toEqual([ + modelStub1, + undefined, + [], + ]); + expect(onModelErrorSpy.calls.argsFor(1)).toEqual([ + modelStub2, + undefined, + [errorRecord1], + ]); + expect(onModelErrorSpy.calls.argsFor(2)).toEqual([ + modelStub3, + undefined, + [errorRecord2, errorRecord3], + ]); + expect(onModelErrorSpy.calls.argsFor(3)).toEqual([ + modelStub4, + undefined, + [], + ]); + + // Post + component.model = modelStub; + component1.model = modelStub; + }); - TestBed.configureTestingModule({ - declarations: [TaurusBaseComponentStub, TaurusBaseComponentStubV1], - providers: [ - { provide: ComponentService, useValue: componentServiceStub }, - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub } - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); + it("should verify will invoke correct Taurus lifecycle hooks for FAILED and modified with Task", () => { + // Given + const task = "delete_entity"; + modelStub = new ComponentModelStub(task) as any; + const onModelInitSpy = spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelInitialLoadSpy = spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelInitialLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelErrorSpy = spyOn( + TaurusBaseComponentStubV1.prototype, + "onModelError", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(FAILED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + const component1: TaurusBaseComponentStubV1 = TestBed.createComponent( + TaurusBaseComponentStubV1, + ).componentInstance; + + // Then 1 + expect(component1.model).toBeUndefined(); + + // When + // @ts-ignore + component1.bindModel(); + + // Then 2 + expect(component1.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelInitialLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelErrorSpy).toHaveBeenCalledWith(modelStub, task, []); + + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + + expect(component1.model).toBe(modelStub); + + expect(onModelChangeSpy).not.toHaveBeenCalled(); + + // Post + component.model = modelStub; + component1.model = modelStub; + }); - fixture = TestBed.createComponent(TaurusBaseComponentStub); - component = fixture.componentInstance; + it("should verify will invoke correct Taurus lifecycle hooks for LOADED and not modified", () => { + // Given + const localModelStub: ComponentModel = new ComponentModelStub() as any; + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + localModelStub, + "isModified", + ).and.returnValue(false); + const modelStatusSpy = spyOnProperty( + localModelStub, + "status", + "get", + ).and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(localModelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith( + localModelStub, + undefined, + ); + expect(onModelLoadSpy).toHaveBeenCalledWith(localModelStub, undefined); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).not.toBe(localModelStub); + expect(modelStatusSpy).not.toHaveBeenCalled(); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it("should verify will invoke correct Taurus lifecycle hooks for LOADED and not modified with Task", () => { + // Given + const task = "move_entity"; + modelStub = new ComponentModelStub() as any; + const localModelStub: ComponentModel = new ComponentModelStub( + task, + ) as any; + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + localModelStub, + "isModified", + ).and.returnValue(false); + const modelStatusSpy = spyOnProperty( + localModelStub, + "status", + "get", + ).and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(localModelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(localModelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(localModelStub, task); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).not.toBe(localModelStub); + expect(modelStatusSpy).not.toHaveBeenCalled(); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it("should verify Taurus lifecycle hook will throw/catch error and log it to console", () => { + // Given + const error = new Error("Random Error"); + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.throwError(error); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(LOADED); + const spyError = spyOn(console, "error").and.callFake(CallFake); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(spyError).toHaveBeenCalledWith( + `Taurus NgRx Redux failed to execute lifecycle hook "onModelChange"!`, + error, + ); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it("should verify Taurus lifecycle hook post normalize will throw/catch error and log it to console", () => { + // Given + const error = new Error("Random Error"); + const onModelInitSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelInit", + ).and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFirstLoad", + ).and.callFake(CallFake); + const onModelLoadSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelLoad", + ).and.callFake(CallFake); + const onModelChangeSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelChange", + ).and.callFake(CallFake); + const onModelFailSpy = spyOn( + TaurusBaseComponentStub.prototype, + "onModelFail", + ).and.callFake(CallFake); + const isModelModifiedSpy = spyOn( + modelStub, + "isModified", + ).and.returnValue(true); + const modelStatusSpy = spyOnProperty( + modelStub, + "status", + "get", + ).and.returnValue(LOADED); + const spyError = spyOn(console, "error").and.callFake(CallFake); + // @ts-ignore + spyOn(component, "normalizeModelState").and.throwError(error); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, undefined); + expect(spyError).toHaveBeenCalledWith( + `Taurus NgRx Redux failed to normalize ComponentModel!`, + error, + ); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); }); - it('should verify component is created', () => { + describe("|setStateIdle|", () => { + it("should verify will invoke correct methods", () => { // Given + const componentState: ComponentState = { + id: "TaurusBaseComponentStub", + status: LOADED, + toLiteral: CallFake, + toLiteralCloneDeep: CallFake, + copy: CallFake, + }; + const prepareForDestroySpy = spyOn( + modelStub, + "prepareForDestroy", + ).and.callFake(() => modelStub); + const getComponentStateSpy = spyOn( + modelStub, + "getComponentState", + ).and.returnValue(componentState); component.model = modelStub; + // When + // @ts-ignore + component.setStateIdle(); + // Then - expect(component).toBeDefined(); - expect(component).toBeInstanceOf(TaurusBaseComponentStub); - expect(component).toBeInstanceOf(TaurusBaseComponent); + expect(prepareForDestroySpy).toHaveBeenCalled(); + expect(getComponentStateSpy).toHaveBeenCalled(); + expect(componentServiceStub.idle).toHaveBeenCalledWith(componentState); + }); }); - describe('Properties::', () => { - describe('|uuid|', () => { - it('should verify default value is undefined', () => { - // Given - component.model = modelStub; - - // When - expect(component.uuid).toEqual('TaurusBaseComponentStub'); - }); - }); - - describe('|componentService|', () => { - it('should verify ComponentService will be injected', () => { - // Given - component.model = modelStub; - - // When - expect(component['componentService']).toBeDefined(); - }); - }); - - describe('|navigationService|', () => { - it('should verify NavigationService will be injected', () => { - // Given - component.model = modelStub; - - // When - expect(component['navigationService']).toBeDefined(); - }); - }); - - describe('|activatedRoute|', () => { - it('should verify ActivatedRoute will be injected', () => { - // Given - component.model = modelStub; - - // When - expect(component['activatedRoute']).toBeDefined(); - }); - }); + describe("|normalizeModelState|", () => { + it("should verify will invoke correct methods", () => { + // Given + const componentState: ComponentState = { + id: "TaurusBaseComponentStub", + status: LOADED, + toLiteral: CallFake, + toLiteralCloneDeep: CallFake, + copy: CallFake, + }; + const clearTaskSpy = spyOn(modelStub, "clearTask").and.callFake( + () => modelStub, + ); + const getComponentStateSpy = spyOn( + modelStub, + "getComponentState", + ).and.returnValue(componentState); + component.model = modelStub; + + // When + // @ts-ignore + component.normalizeModelState(modelStub); + + // Then + expect(clearTaskSpy).toHaveBeenCalled(); + expect(getComponentStateSpy).toHaveBeenCalled(); + expect(componentServiceStub.update).toHaveBeenCalledWith( + componentState, + ); + }); }); - describe('Angular lifecycle hooks::', () => { - beforeEach(() => { - component.model = modelStub; - }); - - describe('|ngOnInit|', () => { - it('should verify will invoke expected method', () => { - // Given - // @ts-ignore - const bindModelSpy = spyOn(component, 'bindModel').and.callFake(CallFake); - - // When - fixture.detectChanges(); - - // Then - expect(bindModelSpy).toHaveBeenCalled(); - - // Post - fixture.componentInstance.model = modelStub; - }); - }); - - describe('|ngOnDestroy|', () => { - it('should verify will invoke expected methods', () => { - // Given - spyOn(TaurusBaseComponent.prototype, 'ngOnInit').and.callFake(CallFake); - // @ts-ignore - const setIdleSpy: jasmine.Spy<() => void> = spyOn(TaurusBaseComponent.prototype, 'setStateIdle').and.callFake(CallFake); - const ngOnDestroySpy: jasmine.Spy<() => void> = spyOn(TaurusObject.prototype, 'ngOnDestroy').and.callFake(CallFake); - - // When - fixture = TestBed.createComponent(TaurusBaseComponentStub); - fixture.componentInstance.model = modelStub; - fixture.detectChanges(); - fixture.destroy(); - - // Then - expect(setIdleSpy).toHaveBeenCalled(); - expect(ngOnDestroySpy).toHaveBeenCalled(); - }); - }); + describe("|refreshModel|", () => { + it("should verify will assign previous model until depth 3", () => { + // Given + const modelStub1: ComponentModel = new ComponentModelStub( + null, + 1, + ) as any; + const modelStub2: ComponentModel = new ComponentModelStub( + null, + 2, + ) as any; + const modelStub3: ComponentModel = new ComponentModelStub( + null, + 3, + ) as any; + const modelStub4: ComponentModel = new ComponentModelStub( + null, + 4, + ) as any; + const modelStub5: ComponentModel = new ComponentModelStub( + null, + 5, + ) as any; + const modelStub6: ComponentModel = new ComponentModelStub( + null, + 6, + ) as any; + component.model = modelStub1; + + // When + // @ts-ignore + component.refreshModel(modelStub2); + // @ts-ignore + component.refreshModel(modelStub3); + // @ts-ignore + component.refreshModel(modelStub4); + // @ts-ignore + component.refreshModel(modelStub5); + // @ts-ignore + component.refreshModel(modelStub6); + + // Then + expect(component.model.previousModel).toBe(modelStub5); + expect(component.model.previousModel.previousModel).toBe(modelStub4); + expect(component.model.previousModel.previousModel.previousModel).toBe( + modelStub3, + ); + expect( + component.model.previousModel.previousModel.previousModel + .previousModel, + ).toBeUndefined(); + }); + + it("should verify will throw/catch error and log to console", () => { + // Given + const error = new Error("Random Error"); + const modelStub1: ComponentModel = new ComponentModelStubV1( + null, + 1, + ) as any; + const modelStub2: ComponentModel = new ComponentModelStubV1( + null, + 2, + ) as any; + const consoleSpy = spyOn(console, "error").and.callFake(CallFake); + spyOnProperty(modelStub1, "previousModel", "get").and.throwError(error); + component.model = modelStub1; + + // When + // @ts-ignore + component.refreshModel(modelStub2); + + // Then + expect(consoleSpy).toHaveBeenCalledWith( + "Failed to clean previous ComponentModel", + error, + ); + }); }); - describe('Methods::', () => { - describe('|navigateTo|', () => { - it('should verify will invoke correct methods', () => { - // Given - const replaceValues = { '$.team': 'team1', '$.entity': 'Prime' }; - navigationServiceStub.navigateTo.and.returnValue(Promise.resolve(true)); - component.model = modelStub; - - // When - const promise = component.navigateTo(replaceValues); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(navigationServiceStub.navigateTo).toHaveBeenCalledWith(replaceValues); - }); - }); - - describe('|navigateBack|', () => { - it('should verify will invoke correct methods', () => { - // Given - const replaceValues = { '$.team': 'team10', '$.entity': 'Second' }; - navigationServiceStub.navigateBack.and.returnValue(Promise.resolve(false)); - component.model = modelStub; - - // When - const promise = component.navigateBack(replaceValues); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(navigationServiceStub.navigateBack).toHaveBeenCalledWith(replaceValues); - }); - }); - - describe('|bindModel|', () => { - it('should verify will create correct subscriptions', () => { - // Given - componentServiceStub.init.and.returnValue(of(modelStub).pipe(take(1))); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(componentServiceStub.init).toHaveBeenCalledWith(component.uuid, routeStateStub); - expect(componentServiceStub.getModel).toHaveBeenCalledWith(component.uuid, routeStateStub.routePathSegments); - expect(component['subscriptions'].length).toEqual(1); // eslint-disable-line @typescript-eslint/no-unsafe-member-access - }); - - it('should verify will invoke correct Taurus lifecycle hooks for LOADED and modified', () => { - // Given - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(LOADED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).toBe(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFailSpy).not.toHaveBeenCalled(); - }); - - it('should verify will invoke correct Taurus lifecycle hooks for LOADED and modified with Task', () => { - // Given - const task = 'delete_entity'; - modelStub = new ComponentModelStub(task) as any; - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(LOADED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).toBe(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelFailSpy).not.toHaveBeenCalled(); - }); - - it('should verify will invoke correct Taurus lifecycle hooks for FAILED and modified deprecated', () => { - // Given - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(FAILED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).toBe(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - expect(onModelFailSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelChangeSpy).not.toHaveBeenCalled(); - }); - - it('should verify will invoke correct Taurus lifecycle hooks for FAILED and modified with Task deprecated', () => { - // Given - const task = 'patch_entity'; - modelStub = new ComponentModelStub(task) as any; - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(FAILED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).toBe(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - expect(onModelFailSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelChangeSpy).not.toHaveBeenCalled(); - }); - - it('should verify will invoke correct Taurus lifecycle hooks for FAILED and modified', () => { - // Given - spyOn(TaurusBaseComponentStubV1.prototype, 'onModelInit').and.callFake(CallFake); - spyOn(TaurusBaseComponentStubV1.prototype, 'onModelInitialLoad').and.callFake(CallFake); - spyOn(TaurusBaseComponentStubV1.prototype, 'onModelLoad').and.callFake(CallFake); - spyOn(TaurusBaseComponentStubV1.prototype, 'onModelChange').and.callFake(CallFake); - const onModelErrorSpy = spyOn(TaurusBaseComponentStubV1.prototype, 'onModelError').and.callFake(CallFake); - - spyOn(modelStub, 'isModified').and.returnValue(true); - spyOnProperty(modelStub, 'status', 'get').and.returnValue(FAILED); - - const modelStub1: ComponentModel = new ComponentModelStub(null, 1) as any; - spyOn(modelStub1, 'isModified').and.returnValue(true); - spyOnProperty(modelStub1, 'status', 'get').and.returnValue(FAILED); - const modelStub2: ComponentModel = new ComponentModelStub(null, 2) as any; - spyOn(modelStub2, 'isModified').and.returnValue(true); - spyOnProperty(modelStub2, 'status', 'get').and.returnValue(FAILED); - const modelStub3: ComponentModel = new ComponentModelStub(null, 3) as any; - spyOn(modelStub3, 'isModified').and.returnValue(true); - spyOnProperty(modelStub3, 'status', 'get').and.returnValue(FAILED); - const modelStub4: ComponentModel = new ComponentModelStub(null, 4) as any; - spyOn(modelStub4, 'isModified').and.returnValue(true); - spyOnProperty(modelStub4, 'status', 'get').and.returnValue(FAILED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - - const modelSubject = new BehaviorSubject(modelStub1); - componentServiceStub.getModel.and.returnValue(modelSubject); - - const component1: TaurusBaseComponentStubV1 = TestBed.createComponent(TaurusBaseComponentStubV1).componentInstance; - - // When - // @ts-ignore - component1.bindModel(); - // @ts-ignore - component1.model = null; - modelSubject.next(modelStub2); - modelSubject.next(modelStub3); - modelSubject.next(modelStub4); - - // Then - expect(onModelErrorSpy.calls.argsFor(0)).toEqual([modelStub1, undefined, []]); - expect(onModelErrorSpy.calls.argsFor(1)).toEqual([modelStub2, undefined, [errorRecord1]]); - expect(onModelErrorSpy.calls.argsFor(2)).toEqual([modelStub3, undefined, [errorRecord2, errorRecord3]]); - expect(onModelErrorSpy.calls.argsFor(3)).toEqual([modelStub4, undefined, []]); - - // Post - component.model = modelStub; - component1.model = modelStub; - }); - - it('should verify will invoke correct Taurus lifecycle hooks for FAILED and modified with Task', () => { - // Given - const task = 'delete_entity'; - modelStub = new ComponentModelStub(task) as any; - const onModelInitSpy = spyOn(TaurusBaseComponentStubV1.prototype, 'onModelInit').and.callFake(CallFake); - const onModelInitialLoadSpy = spyOn(TaurusBaseComponentStubV1.prototype, 'onModelInitialLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStubV1.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStubV1.prototype, 'onModelChange').and.callFake(CallFake); - const onModelErrorSpy = spyOn(TaurusBaseComponentStubV1.prototype, 'onModelError').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(FAILED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - const component1: TaurusBaseComponentStubV1 = TestBed.createComponent(TaurusBaseComponentStubV1).componentInstance; - - // Then 1 - expect(component1.model).toBeUndefined(); - - // When - // @ts-ignore - component1.bindModel(); - - // Then 2 - expect(component1.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelInitialLoadSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); - expect(onModelErrorSpy).toHaveBeenCalledWith(modelStub, task, []); - - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - - expect(component1.model).toBe(modelStub); - - expect(onModelChangeSpy).not.toHaveBeenCalled(); - - // Post - component.model = modelStub; - component1.model = modelStub; - }); - - it('should verify will invoke correct Taurus lifecycle hooks for LOADED and not modified', () => { - // Given - const localModelStub: ComponentModel = new ComponentModelStub() as any; - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(localModelStub, 'isModified').and.returnValue(false); - const modelStatusSpy = spyOnProperty(localModelStub, 'status', 'get').and.returnValue(LOADED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(localModelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(localModelStub, undefined); - expect(onModelLoadSpy).toHaveBeenCalledWith(localModelStub, undefined); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).not.toBe(localModelStub); - expect(modelStatusSpy).not.toHaveBeenCalled(); - expect(onModelChangeSpy).not.toHaveBeenCalled(); - expect(onModelFailSpy).not.toHaveBeenCalled(); - }); - - it('should verify will invoke correct Taurus lifecycle hooks for LOADED and not modified with Task', () => { - // Given - const task = 'move_entity'; - modelStub = new ComponentModelStub() as any; - const localModelStub: ComponentModel = new ComponentModelStub(task) as any; - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(localModelStub, 'isModified').and.returnValue(false); - const modelStatusSpy = spyOnProperty(localModelStub, 'status', 'get').and.returnValue(LOADED); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(localModelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(localModelStub, task); - expect(onModelLoadSpy).toHaveBeenCalledWith(localModelStub, task); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).not.toBe(localModelStub); - expect(modelStatusSpy).not.toHaveBeenCalled(); - expect(onModelChangeSpy).not.toHaveBeenCalled(); - expect(onModelFailSpy).not.toHaveBeenCalled(); - }); - - it('should verify Taurus lifecycle hook will throw/catch error and log it to console', () => { - // Given - const error = new Error('Random Error'); - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.throwError(error); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(LOADED); - const spyError = spyOn(console, 'error').and.callFake(CallFake); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).toBe(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(spyError).toHaveBeenCalledWith(`Taurus NgRx Redux failed to execute lifecycle hook "onModelChange"!`, error); - expect(onModelFailSpy).not.toHaveBeenCalled(); - }); - - it('should verify Taurus lifecycle hook post normalize will throw/catch error and log it to console', () => { - // Given - const error = new Error('Random Error'); - const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); - const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); - const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); - const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); - const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); - const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); - const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(LOADED); - const spyError = spyOn(console, 'error').and.callFake(CallFake); - // @ts-ignore - spyOn(component, 'normalizeModelState').and.throwError(error); - - componentServiceStub.init.and.returnValue(of(modelStub)); - componentServiceStub.getModel.and.returnValue(of(modelStub)); - - // Then 1 - expect(component.model).toBeUndefined(); - - // When - // @ts-ignore - component.bindModel(); - - // Then 2 - expect(component.model).toBe(modelStub); - expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); - expect(component.model).toBe(modelStub); - expect(modelStatusSpy).toHaveBeenCalled(); - expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, undefined); - expect(spyError).toHaveBeenCalledWith(`Taurus NgRx Redux failed to normalize ComponentModel!`, error); - expect(onModelFailSpy).not.toHaveBeenCalled(); - }); - }); - - describe('|setStateIdle|', () => { - it('should verify will invoke correct methods', () => { - // Given - const componentState: ComponentState = { - id: 'TaurusBaseComponentStub', - status: LOADED, - toLiteral: CallFake, - toLiteralCloneDeep: CallFake, - copy: CallFake - }; - const prepareForDestroySpy = spyOn(modelStub, 'prepareForDestroy').and.callFake(() => modelStub); - const getComponentStateSpy = spyOn(modelStub, 'getComponentState').and.returnValue(componentState); - component.model = modelStub; - - // When - // @ts-ignore - component.setStateIdle(); - - // Then - expect(prepareForDestroySpy).toHaveBeenCalled(); - expect(getComponentStateSpy).toHaveBeenCalled(); - expect(componentServiceStub.idle).toHaveBeenCalledWith(componentState); - }); - }); - - describe('|normalizeModelState|', () => { - it('should verify will invoke correct methods', () => { - // Given - const componentState: ComponentState = { - id: 'TaurusBaseComponentStub', - status: LOADED, - toLiteral: CallFake, - toLiteralCloneDeep: CallFake, - copy: CallFake - }; - const clearTaskSpy = spyOn(modelStub, 'clearTask').and.callFake(() => modelStub); - const getComponentStateSpy = spyOn(modelStub, 'getComponentState').and.returnValue(componentState); - component.model = modelStub; - - // When - // @ts-ignore - component.normalizeModelState(modelStub); - - // Then - expect(clearTaskSpy).toHaveBeenCalled(); - expect(getComponentStateSpy).toHaveBeenCalled(); - expect(componentServiceStub.update).toHaveBeenCalledWith(componentState); - }); - }); - - describe('|refreshModel|', () => { - it('should verify will assign previous model until depth 3', () => { - // Given - const modelStub1: ComponentModel = new ComponentModelStub(null, 1) as any; - const modelStub2: ComponentModel = new ComponentModelStub(null, 2) as any; - const modelStub3: ComponentModel = new ComponentModelStub(null, 3) as any; - const modelStub4: ComponentModel = new ComponentModelStub(null, 4) as any; - const modelStub5: ComponentModel = new ComponentModelStub(null, 5) as any; - const modelStub6: ComponentModel = new ComponentModelStub(null, 6) as any; - component.model = modelStub1; - - // When - // @ts-ignore - component.refreshModel(modelStub2); - // @ts-ignore - component.refreshModel(modelStub3); - // @ts-ignore - component.refreshModel(modelStub4); - // @ts-ignore - component.refreshModel(modelStub5); - // @ts-ignore - component.refreshModel(modelStub6); - - // Then - expect(component.model.previousModel).toBe(modelStub5); - expect(component.model.previousModel.previousModel).toBe(modelStub4); - expect(component.model.previousModel.previousModel.previousModel).toBe(modelStub3); - expect(component.model.previousModel.previousModel.previousModel.previousModel).toBeUndefined(); - }); - - it('should verify will throw/catch error and log to console', () => { - // Given - const error = new Error('Random Error'); - const modelStub1: ComponentModel = new ComponentModelStubV1(null, 1) as any; - const modelStub2: ComponentModel = new ComponentModelStubV1(null, 2) as any; - const consoleSpy = spyOn(console, 'error').and.callFake(CallFake); - spyOnProperty(modelStub1, 'previousModel', 'get').and.throwError(error); - component.model = modelStub1; - - // When - // @ts-ignore - component.refreshModel(modelStub2); - - // Then - expect(consoleSpy).toHaveBeenCalledWith('Failed to clean previous ComponentModel', error); - }); - }); - - describe('|initializeRouteReuse|', () => { - it('should verify if route change event is fired will bind new model', () => { - // Given - const componentFixture1 = TestBed.createComponent(TaurusBaseComponentStubV1); - const component1 = componentFixture1.componentInstance; - const modelStub1 = new ComponentModelStubV1(null, 1) as any; - const modelStub2 = new ComponentModelStubV1(null, 2) as any; - - // @ts-ignore - const spyBindModel = spyOn(component1, 'bindModel').and.callThrough(); - // @ts-ignore - const spySetStateIdle = spyOn(component1, 'setStateIdle').and.callThrough(); - - component1.model = modelStub1; - componentServiceStub.init.and.returnValues(of(modelStub1), of(modelStub2)); - componentServiceStub.getModel.and.returnValues(of(modelStub1), of(modelStub2)); - - componentFixture1.detectChanges(); - - // When - paramsSubject.next({ asset: 10, name: 'Jack' }); - - // Then - expect(component1.model).toBe(modelStub2); - expect(spyBindModel).toHaveBeenCalledTimes(2); - expect(spySetStateIdle).toHaveBeenCalledTimes(1); - - // Post - component.model = modelStub; - }); - }); + describe("|initializeRouteReuse|", () => { + it("should verify if route change event is fired will bind new model", () => { + // Given + const componentFixture1 = TestBed.createComponent( + TaurusBaseComponentStubV1, + ); + const component1 = componentFixture1.componentInstance; + const modelStub1 = new ComponentModelStubV1(null, 1) as any; + const modelStub2 = new ComponentModelStubV1(null, 2) as any; + + // @ts-ignore + const spyBindModel = spyOn(component1, "bindModel").and.callThrough(); + // @ts-ignore + const spySetStateIdle = spyOn( + component1, + "setStateIdle", + ).and.callThrough(); + + component1.model = modelStub1; + componentServiceStub.init.and.returnValues( + of(modelStub1), + of(modelStub2), + ); + componentServiceStub.getModel.and.returnValues( + of(modelStub1), + of(modelStub2), + ); + + componentFixture1.detectChanges(); + + // When + paramsSubject.next({ asset: 10, name: "Jack" }); + + // Then + expect(component1.model).toBe(modelStub2); + expect(spyBindModel).toHaveBeenCalledTimes(2); + expect(spySetStateIdle).toHaveBeenCalledTimes(1); + + // Post + component.model = modelStub; + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.ts index 6ee8a3d3ac..899f7ed220 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/components/redux-base/taurus-base.component.ts @@ -5,366 +5,422 @@ /* eslint-disable @angular-eslint/directive-class-suffix */ -import { Directive, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; +import { Directive, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Params } from "@angular/router"; -import { Subscription } from 'rxjs'; +import { Subscription } from "rxjs"; -import { ArrayElement, CollectionsUtil } from '../../../../utils'; +import { ArrayElement, CollectionsUtil } from "../../../../utils"; -import { ErrorRecord, TaurusNavigateAction } from '../../../../common'; +import { ErrorRecord, TaurusNavigateAction } from "../../../../common"; -import { NavigationService } from '../../../navigation'; -import { RouteStateFactory } from '../../../router/factory'; +import { NavigationService } from "../../../navigation"; +import { RouteStateFactory } from "../../../router/factory"; -import { ComponentModel, FAILED } from '../../model'; -import { ComponentService } from '../../services'; +import { ComponentModel, FAILED } from "../../model"; +import { ComponentService } from "../../services"; -import { TaurusErrorBaseComponent } from '../error-base'; +import { TaurusErrorBaseComponent } from "../error-base"; -import { TaurusComponentHooks } from './interfaces'; +import { TaurusComponentHooks } from "./interfaces"; /** * ** Superclass Component for all other Components that want to use NgRx Store and all lifecycle hooks from Taurus. */ @Directive() -export abstract class TaurusBaseComponent extends TaurusErrorBaseComponent implements OnInit, OnDestroy { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'TaurusBaseComponent'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Taurus-Base-Component'; - - private static _routeStateFactory: RouteStateFactory; - - /** - * ** Field that hold Component Model. - */ - model: ComponentModel; - - /** - * ** UUID is identifier for every Subclass in Components state Store. - */ - abstract readonly uuid: string; - - /** - * ** Feature flag to enforce Route reuse in native way provided from Taurus NgRx. - * - * - Introduced for backward compatibility. - * - Default value is false, and continues to operate like previous major version. - * - If set to true will enforce Route reuse strategy and will re-initialize Component with new Model for the new params. - */ - enforceRouteReuse = false; - - /** - * ** Model subscription ref. - * @private - */ - private _modelSubscription: Subscription; - - /** - * ** Constructor. - */ - protected constructor( - protected readonly componentService: ComponentService, - protected readonly navigationService: NavigationService, - protected readonly activatedRoute: ActivatedRoute, - className: string = null - ) { - super(className ?? TaurusBaseComponent.CLASS_NAME); +export abstract class TaurusBaseComponent + extends TaurusErrorBaseComponent + implements OnInit, OnDestroy +{ + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "TaurusBaseComponent"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Taurus-Base-Component"; + + private static _routeStateFactory: RouteStateFactory; + + /** + * ** Field that hold Component Model. + */ + model: ComponentModel; + + /** + * ** UUID is identifier for every Subclass in Components state Store. + */ + abstract readonly uuid: string; + + /** + * ** Feature flag to enforce Route reuse in native way provided from Taurus NgRx. + * + * - Introduced for backward compatibility. + * - Default value is false, and continues to operate like previous major version. + * - If set to true will enforce Route reuse strategy and will re-initialize Component with new Model for the new params. + */ + enforceRouteReuse = false; + + /** + * ** Model subscription ref. + * @private + */ + private _modelSubscription: Subscription; + + /** + * ** Constructor. + */ + protected constructor( + protected readonly componentService: ComponentService, + protected readonly navigationService: NavigationService, + protected readonly activatedRoute: ActivatedRoute, + className: string = null, + ) { + super(className ?? TaurusBaseComponent.CLASS_NAME); + } + + /** + * ** Navigate to page using {@link NavigationService.navigateTo}. + */ + navigateTo(replaceValues?: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }): Promise { + return this.navigationService.navigateTo(replaceValues); + } + + /** + * ** Navigate back to previous page using {@link NavigationService.navigateBack}. + */ + navigateBack(replaceValues?: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }): Promise { + return this.navigationService.navigateBack(replaceValues); + } + + /** + * @inheritDoc + */ + ngOnInit() { + this.bindModel(); + + this.initializeRouteReuse(); + } + + /** + * @inheritDoc + */ + override ngOnDestroy() { + this.setStateIdle(); + + super.ngOnDestroy(); + } + + /** + * ** Invoking method register subscriber for Taurus NgRx Redux Store mutation in context of {@link ComponentState.routePathSegments}, + * which binds {@link ComponentModel} to {@link TaurusBaseComponent.model} + * and start invocation of Taurus NgRx Redux Component lifecycle hooks. + * + * Invocation order: + * + * 1. {@link OnTaurusModelInit} + * 2. {@link OnTaurusModelInitialLoad} or {@link OnTaurusModelFirstLoad} - only one could be invoke, + * where deprecated shouldn't be implemented anymore. + * 3. {@link OnTaurusModelLoad} + * 4. {@link OnTaurusModelChange} when status is {@link LOADED} + * 5. {@link OnTaurusModelError} or {@link OnTaurusModelFail} when status is {@link FAILED} - only one could be invoke, + * where deprecated shouldn't be implemented anymore. + * + *

    + *
    + * Override it if you want to change default behavior. + *

    + * + * @protected + */ + protected bindModel(): void { + let isOnModelInitialLoadExecuted = false; + + if (!TaurusBaseComponent._routeStateFactory) { + TaurusBaseComponent._routeStateFactory = new RouteStateFactory(); } - /** - * ** Navigate to page using {@link NavigationService.navigateTo}. - */ - navigateTo(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }): Promise { - return this.navigationService.navigateTo(replaceValues); - } + const routeState = TaurusBaseComponent._routeStateFactory.create( + this.activatedRoute.snapshot, + null, + ); + + this.componentService.init(this.uuid, routeState).subscribe((model) => { + this.model = model; + + TaurusBaseComponent._executeTaurusComponentHook( + this, + "onModelInit", + model, + ); + }); + + this._modelSubscription = this.componentService + .getModel(this.uuid, routeState.routePathSegments) + .subscribe((model) => { + if (!isOnModelInitialLoadExecuted) { + isOnModelInitialLoadExecuted = true; + if ( + !TaurusBaseComponent._executeTaurusComponentHook( + this, + "onModelInitialLoad", + model, + ) + ) { + TaurusBaseComponent._executeTaurusComponentHook( + this, + "onModelFirstLoad", + model, + ); + } + } - /** - * ** Navigate back to previous page using {@link NavigationService.navigateBack}. - */ - navigateBack(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }): Promise { - return this.navigationService.navigateBack(replaceValues); + this.evaluateTaurusComponentLifecycleHooks(model); + }); + + this.subscriptions.push(this._modelSubscription); + } + + /** + * ** Evaluates Taurus NgRx Redux Component lifecycle hooks + * ({@link OnTaurusModelLoad} and {@link OnTaurusModelChange} and ({@link OnTaurusModelFail} or {@link OnTaurusModelError})). + * + * - Override it if you want to change default behavior. + * + * @protected + */ + protected evaluateTaurusComponentLifecycleHooks(model: ComponentModel): void { + TaurusBaseComponent._executeTaurusComponentHook(this, "onModelLoad", model); + + if (!this.isModelModified(model)) { + return; } - /** - * @inheritDoc - */ - ngOnInit() { - this.bindModel(); - - this.initializeRouteReuse(); + const previousModel = this.model; + + this.refreshModel(model); + + if (model.status === FAILED) { + const previousErrorRecords: ErrorRecord[] = + previousModel instanceof ComponentModel + ? previousModel.getComponentState().errors.records + : []; + const distinctErrorRecordsFromPreviousCycle = model + .getComponentState() + .errors.distinctErrorRecords(previousErrorRecords); + + if ( + !TaurusBaseComponent._executeTaurusComponentHook( + this, + "onModelError", + model, + distinctErrorRecordsFromPreviousCycle, + ) + ) { + TaurusBaseComponent._executeTaurusComponentHook( + this, + "onModelFail", + model, + ); + } + } else { + TaurusBaseComponent._executeTaurusComponentHook( + this, + "onModelChange", + model, + ); } - /** - * @inheritDoc - */ - override ngOnDestroy() { - this.setStateIdle(); - - super.ngOnDestroy(); + try { + this.normalizeModelState(model); + } catch (e) { + console.error(`Taurus NgRx Redux failed to normalize ComponentModel!`, e); } - - /** - * ** Invoking method register subscriber for Taurus NgRx Redux Store mutation in context of {@link ComponentState.routePathSegments}, - * which binds {@link ComponentModel} to {@link TaurusBaseComponent.model} - * and start invocation of Taurus NgRx Redux Component lifecycle hooks. - * - * Invocation order: - * - * 1. {@link OnTaurusModelInit} - * 2. {@link OnTaurusModelInitialLoad} or {@link OnTaurusModelFirstLoad} - only one could be invoke, - * where deprecated shouldn't be implemented anymore. - * 3. {@link OnTaurusModelLoad} - * 4. {@link OnTaurusModelChange} when status is {@link LOADED} - * 5. {@link OnTaurusModelError} or {@link OnTaurusModelFail} when status is {@link FAILED} - only one could be invoke, - * where deprecated shouldn't be implemented anymore. - * - *

    - *
    - * Override it if you want to change default behavior. - *

    - * - * @protected - */ - protected bindModel(): void { - let isOnModelInitialLoadExecuted = false; - - if (!TaurusBaseComponent._routeStateFactory) { - TaurusBaseComponent._routeStateFactory = new RouteStateFactory(); - } - - const routeState = TaurusBaseComponent._routeStateFactory.create(this.activatedRoute.snapshot, null); - - this.componentService.init(this.uuid, routeState).subscribe((model) => { - this.model = model; - - TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelInit', model); - }); - - this._modelSubscription = this.componentService.getModel(this.uuid, routeState.routePathSegments).subscribe((model) => { - if (!isOnModelInitialLoadExecuted) { - isOnModelInitialLoadExecuted = true; - if (!TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelInitialLoad', model)) { - TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelFirstLoad', model); - } - } - - this.evaluateTaurusComponentLifecycleHooks(model); - }); - - this.subscriptions.push(this._modelSubscription); - } - - /** - * ** Evaluates Taurus NgRx Redux Component lifecycle hooks - * ({@link OnTaurusModelLoad} and {@link OnTaurusModelChange} and ({@link OnTaurusModelFail} or {@link OnTaurusModelError})). - * - * - Override it if you want to change default behavior. - * - * @protected - */ - protected evaluateTaurusComponentLifecycleHooks(model: ComponentModel): void { - TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelLoad', model); - - if (!this.isModelModified(model)) { - return; - } - - const previousModel = this.model; - - this.refreshModel(model); - - if (model.status === FAILED) { - const previousErrorRecords: ErrorRecord[] = - previousModel instanceof ComponentModel ? previousModel.getComponentState().errors.records : []; - const distinctErrorRecordsFromPreviousCycle = model.getComponentState().errors.distinctErrorRecords(previousErrorRecords); - - if (!TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelError', model, distinctErrorRecordsFromPreviousCycle)) { - TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelFail', model); - } + } + + /** + * ** Refresh model field {@link TaurusBaseComponent.model} with new one, + * and assigns previous model reference to field {@link ComponentModel.previousModel} in the new model, + * to the max depth 3. + * + * - All assignments are by reference. + * - Override it if you want to change default behavior. + * - Be cautious about your changes and intents! It could affect features thar depend on this method Impl. + * + * @protected + */ + protected refreshModel(model: ComponentModel): void { + // purge component ErrorStore with data from ComponentModel ErrorStore + this.errors.purge(model.getComponentState().errors); + + // assign current model as previous to the new one + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + model["previousModel"] = this.model; // eslint-disable-line @typescript-eslint/dot-notation + + // clean previous models that exceed max depth 3 + try { + let depthLevel = 1; + let previousModel = model.previousModel; + + while (previousModel instanceof ComponentModel) { + if (++depthLevel <= 3) { + previousModel = previousModel.previousModel; } else { - TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelChange', model); - } - - try { - this.normalizeModelState(model); - } catch (e) { - console.error(`Taurus NgRx Redux failed to normalize ComponentModel!`, e); - } - } + if (previousModel.previousModel) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete previousModel["previousModel"]; // eslint-disable-line @typescript-eslint/dot-notation + } - /** - * ** Refresh model field {@link TaurusBaseComponent.model} with new one, - * and assigns previous model reference to field {@link ComponentModel.previousModel} in the new model, - * to the max depth 3. - * - * - All assignments are by reference. - * - Override it if you want to change default behavior. - * - Be cautious about your changes and intents! It could affect features thar depend on this method Impl. - * - * @protected - */ - protected refreshModel(model: ComponentModel): void { - // purge component ErrorStore with data from ComponentModel ErrorStore - this.errors.purge(model.getComponentState().errors); - - // assign current model as previous to the new one - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - model['previousModel'] = this.model; // eslint-disable-line @typescript-eslint/dot-notation - - // clean previous models that exceed max depth 3 - try { - let depthLevel = 1; - let previousModel = model.previousModel; - - while (previousModel instanceof ComponentModel) { - if (++depthLevel <= 3) { - previousModel = previousModel.previousModel; - } else { - if (previousModel.previousModel) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete previousModel['previousModel']; // eslint-disable-line @typescript-eslint/dot-notation - } - - break; - } - } - } catch (e) { - console.error('Failed to clean previous ComponentModel', e); + break; } - - // assign model as current - this.model = model; + } + } catch (e) { + console.error("Failed to clean previous ComponentModel", e); } - /** - * ** Normalize Model state, by default it clear Task field in {@link ComponentState.task} and update model in Taurus NgRx Redux Store. - * - * - It is invoked only if {@link ComponentModel} is modified and after invocation of all Taurus components lifecycle hooks. - * - Override it if you want to change default behavior. - * - * @protected - */ - protected normalizeModelState(model: ComponentModel): void { - this.componentService.update(model.clearTask().getComponentState()); + // assign model as current + this.model = model; + } + + /** + * ** Normalize Model state, by default it clear Task field in {@link ComponentState.task} and update model in Taurus NgRx Redux Store. + * + * - It is invoked only if {@link ComponentModel} is modified and after invocation of all Taurus components lifecycle hooks. + * - Override it if you want to change default behavior. + * + * @protected + */ + protected normalizeModelState(model: ComponentModel): void { + this.componentService.update(model.clearTask().getComponentState()); + } + + /** + * ** Set Model state in IDLE to stop listening on Events from Store. + * + * - It is invoked by default before Component is destroyed, in Angular lifecycle hook {@link OnDestroy}. + * - Override it if you want to change default behavior. + * + * @protected + */ + protected setStateIdle(): void { + this.componentService.idle( + this.model.prepareForDestroy().getComponentState(), + ); + } + + /** + * ** Evaluation of this method acknowledge that new {@link ComponentModel} is modified or not. + * + * - Comparison is evaluated between provided Model and assigned Component's Model {@link TaurusBaseComponent.model}. + * - Default implementation use {@link ComponentModel.isModified} for deep Comparison of specific fields. + * - Override it if you want to change default behavior. + * + *

    + * Be cautious about your changes and intents! + * Examples what can wrong comparison do? + *

    + * + * 1. Infinite lifecycle hooks invocation, where consequences are: performance deterioration or application freeze. + * 2. Prevent lifecycle hooks invocation, where consequences are: your Data never arrive to your Component fields. + * + * @protected + */ + protected isModelModified(model: ComponentModel): boolean { + return model.isModified(this.model); + } + + /** + * ** Initialize Route reuse strategy for Component in context of Taurus NgRx. + * - Turns on listener for Activated params change and if detects mutation + * sets current model in current RoutePathSegment to idle, + * and bind for new model stream to the new RoutePathSegment. + * - New feature this is completely backward compatible, + * and it can be turned on with feature flag per Component Class. + * + * @protected + */ + protected initializeRouteReuse(): void { + if (!this.enforceRouteReuse) { + return; } - /** - * ** Set Model state in IDLE to stop listening on Events from Store. - * - * - It is invoked by default before Component is destroyed, in Angular lifecycle hook {@link OnDestroy}. - * - Override it if you want to change default behavior. - * - * @protected - */ - protected setStateIdle(): void { - this.componentService.idle(this.model.prepareForDestroy().getComponentState()); - } + let previousParams: Params; - /** - * ** Evaluation of this method acknowledge that new {@link ComponentModel} is modified or not. - * - * - Comparison is evaluated between provided Model and assigned Component's Model {@link TaurusBaseComponent.model}. - * - Default implementation use {@link ComponentModel.isModified} for deep Comparison of specific fields. - * - Override it if you want to change default behavior. - * - *

    - * Be cautious about your changes and intents! - * Examples what can wrong comparison do? - *

    - * - * 1. Infinite lifecycle hooks invocation, where consequences are: performance deterioration or application freeze. - * 2. Prevent lifecycle hooks invocation, where consequences are: your Data never arrive to your Component fields. - * - * @protected - */ - protected isModelModified(model: ComponentModel): boolean { - return model.isModified(this.model); - } + this.subscriptions.push( + this.activatedRoute.params.subscribe((params) => { + if (CollectionsUtil.isNil(previousParams)) { + previousParams = params; - /** - * ** Initialize Route reuse strategy for Component in context of Taurus NgRx. - * - Turns on listener for Activated params change and if detects mutation - * sets current model in current RoutePathSegment to idle, - * and bind for new model stream to the new RoutePathSegment. - * - New feature this is completely backward compatible, - * and it can be turned on with feature flag per Component Class. - * - * @protected - */ - protected initializeRouteReuse(): void { - if (!this.enforceRouteReuse) { - return; + return; } - let previousParams: Params; - - this.subscriptions.push( - this.activatedRoute.params.subscribe((params) => { - if (CollectionsUtil.isNil(previousParams)) { - previousParams = params; - - return; - } - - if (!CollectionsUtil.isEqual(previousParams, params)) { - previousParams = params; - - const isRemoveSuccessful = this.removeSubscriptionRef(this._modelSubscription); - if (isRemoveSuccessful) { - // set current RoutePathSegment model state to idle - this.setStateIdle(); - // bind new model stream to new RoutePathSegment - this.bindModel(); - } - } - }) - ); - } - - /** - * ** Invoke Taurus NgRx Redux Component lifecycle hook. - * - * - Lifecycle hooks are invoked only if implementation they are found as implemented in subclasses. - * - Returns true if method is found and executed, otherwise false. - * - * @private - */ - // eslint-disable-next-line @typescript-eslint/member-ordering - private static _executeTaurusComponentHook( - instance: TaurusBaseComponent, - method: keyof TaurusComponentHooks, - model: ComponentModel, - ...additionalParams: any[] - ): boolean { - // eslint-disable-line @typescript-eslint/no-explicit-any - - if (CollectionsUtil.isFunction(instance[method])) { - const currentTask = model.getTask(); - - try { - if (CollectionsUtil.isString(currentTask)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - instance[method](model, currentTask, ...additionalParams); - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - instance[method](model, undefined, ...additionalParams); - } - } catch (e) { - console.error(`Taurus NgRx Redux failed to execute lifecycle hook "${method}"!`, e); - } - - return true; + if (!CollectionsUtil.isEqual(previousParams, params)) { + previousParams = params; + + const isRemoveSuccessful = this.removeSubscriptionRef( + this._modelSubscription, + ); + if (isRemoveSuccessful) { + // set current RoutePathSegment model state to idle + this.setStateIdle(); + // bind new model stream to new RoutePathSegment + this.bindModel(); + } } + }), + ); + } + + /** + * ** Invoke Taurus NgRx Redux Component lifecycle hook. + * + * - Lifecycle hooks are invoked only if implementation they are found as implemented in subclasses. + * - Returns true if method is found and executed, otherwise false. + * + * @private + */ + // eslint-disable-next-line @typescript-eslint/member-ordering + private static _executeTaurusComponentHook( + instance: TaurusBaseComponent, + method: keyof TaurusComponentHooks, + model: ComponentModel, + ...additionalParams: any[] + ): boolean { + // eslint-disable-line @typescript-eslint/no-explicit-any + + if (CollectionsUtil.isFunction(instance[method])) { + const currentTask = model.getTask(); + + try { + if (CollectionsUtil.isString(currentTask)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + instance[method](model, currentTask, ...additionalParams); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + instance[method](model, undefined, ...additionalParams); + } + } catch (e) { + console.error( + `Taurus NgRx Redux failed to execute lifecycle hook "${method}"!`, + e, + ); + } - return false; + return true; } + + return false; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts index 2df29c5b0a..5070f405a7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './components'; -export * from './model'; -export * from './services'; -export * from './state'; +export * from "./components"; +export * from "./model"; +export * from "./services"; +export * from "./state"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts index 1e407429d4..5ab086d12e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts @@ -3,237 +3,280 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { ComparableImpl, generateErrorCode } from '../../../common'; +import { ComparableImpl, generateErrorCode } from "../../../common"; -import { ErrorStoreImpl } from '../../error'; +import { ErrorStoreImpl } from "../../error"; -import { RouterState, RouteSegments, RouteState } from '../../router'; +import { RouterState, RouteSegments, RouteState } from "../../router"; -import { ComponentState, ComponentStateImpl, IDLE, LOADED } from './state'; +import { ComponentState, ComponentStateImpl, IDLE, LOADED } from "./state"; -import { ComponentModel } from './component.model'; +import { ComponentModel } from "./component.model"; -import { ComponentModelComparable } from './component-model.comparable'; +import { ComponentModelComparable } from "./component-model.comparable"; + +describe("ComponentModelComparable", () => { + it("should verify instance is created", () => { + // When + const instance = new ComponentModelComparable(null); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = ComponentModelComparable.of(null); + + // Then + expect(instance).toBeInstanceOf(ComponentModelComparable); + expect(instance).toBeInstanceOf(ComparableImpl); + }); + }); + }); + }); + + describe("Methods::", () => { + let componentState: ComponentState; + let routerState: RouterState; + let modelComparable: ComponentModelComparable; + + beforeEach(() => { + componentState = ComponentStateImpl.of({ + id: "testId", + status: IDLE, + data: new Map([ + ["countries", ["aCountry", "bCountry", "cCountry"]], + ]), + }); + routerState = RouterState.of( + RouteState.of( + RouteSegments.of( + "entity/117", + {}, + { entityId: "107" }, + { search: "test-team-107" }, + RouteSegments.of( + "domain/context", + {}, + {}, + { search: "test-team-107" }, + ), + ), + "domain/context/entity/117", + ), + 43, + ); + modelComparable = ComponentModelComparable.of( + ComponentModel.of(componentState, routerState), + ); + }); + + describe("|compare|", () => { + it("should verify will return -1 when stored comparable value is not instance of ComponentModel", () => { + // Given + modelComparable = ComponentModelComparable.of(null); -describe('ComponentModelComparable', () => { - it('should verify instance is created', () => { // When - const instance = new ComponentModelComparable(null); + const compareV = modelComparable.compare( + ComponentModelComparable.of(undefined), + ); // Then - expect(instance).toBeDefined(); - }); + expect(compareV).toEqual(-1); + }); + + it("should verify will return -1 when provided comparable is not instance of expected", () => { + // When + const compareV = modelComparable.compare( + ComponentModelComparable.of(null), + ); + + // Then + expect(compareV).toEqual(-1); + }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentModelComparable.of(null); - - // Then - expect(instance).toBeInstanceOf(ComponentModelComparable); - expect(instance).toBeInstanceOf(ComparableImpl); - }); - }); + it("should verify will return -1 when no comparable model provided", () => { + // When + const compareV = modelComparable.compare(null); + + // Then + expect(compareV).toEqual(-1); + }); + + it("should verify will return 0 when ComponentModel ref is same", () => { + // Given + const isEqualSpy = spyOn(CollectionsUtil, "isEqual").and.callThrough(); + + // When + modelComparable.value.withPage(10, 100); + modelComparable.value + .getComponentState() + .uiState.set("submitBtn", { state: "submitted" }); + const compareV = modelComparable.compare(modelComparable); + + // Then + expect(compareV).toEqual(0); + expect(isEqualSpy).not.toHaveBeenCalled(); + }); + + it("should verify will return -1 when status is different", () => { + // Given + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + status: LOADED, }); - }); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentStateComparable, routerState), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + }); + + it("should verify will return -1 when task is different", () => { + // Given + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + task: "patch_entity", + }); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentStateComparable, routerState), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + }); + + it("should verify will return 0 when navigationId is different", () => { + // Given + const routerStateComparable = RouterState.of(routerState.state, 90); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentState, routerStateComparable), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(0); + }); + + it("should verify will return -1 when error is different", () => { + // Given + const isEqualSpy = spyOn(CollectionsUtil, "isEqual").and.callThrough(); + + const className = CollectionsUtil.generateRandomString(); + const publicName = CollectionsUtil.generateRandomString(); + const objectUUID = CollectionsUtil.generateObjectUUID(className); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + errors: ErrorStoreImpl.of([ + { + error: new Error("new Error()"), + code: generateErrorCode(className, publicName, "load", "503"), + objectUUID, + httpStatusCode: 503, + time: Date.now(), + }, + ]), + }); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentStateComparable, routerState), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); - describe('Methods::', () => { - let componentState: ComponentState; - let routerState: RouterState; - let modelComparable: ComponentModelComparable; - - beforeEach(() => { - componentState = ComponentStateImpl.of({ - id: 'testId', - status: IDLE, - data: new Map([['countries', ['aCountry', 'bCountry', 'cCountry']]]) - }); - routerState = RouterState.of( - RouteState.of( - RouteSegments.of( - 'entity/117', - {}, - { entityId: '107' }, - { search: 'test-team-107' }, - RouteSegments.of('domain/context', {}, {}, { search: 'test-team-107' }) - ), - 'domain/context/entity/117' - ), - 43 - ); - modelComparable = ComponentModelComparable.of(ComponentModel.of(componentState, routerState)); + // Then + expect(compareV).toEqual(-1); + expect(isEqualSpy).not.toHaveBeenCalled(); + }); + + it("should verify will return 0 when data ref is same", () => { + // Given + const isEqualSpy = spyOn(CollectionsUtil, "isEqual").and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + data: componentState.data, }); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentStateComparable, routerState), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); - describe('|compare|', () => { - it('should verify will return -1 when stored comparable value is not instance of ComponentModel', () => { - // Given - modelComparable = ComponentModelComparable.of(null); - - // When - const compareV = modelComparable.compare(ComponentModelComparable.of(undefined)); - - // Then - expect(compareV).toEqual(-1); - }); - - it('should verify will return -1 when provided comparable is not instance of expected', () => { - // When - const compareV = modelComparable.compare(ComponentModelComparable.of(null)); - - // Then - expect(compareV).toEqual(-1); - }); - - it('should verify will return -1 when no comparable model provided', () => { - // When - const compareV = modelComparable.compare(null); - - // Then - expect(compareV).toEqual(-1); - }); - - it('should verify will return 0 when ComponentModel ref is same', () => { - // Given - const isEqualSpy = spyOn(CollectionsUtil, 'isEqual').and.callThrough(); - - // When - modelComparable.value.withPage(10, 100); - modelComparable.value.getComponentState().uiState.set('submitBtn', { state: 'submitted' }); - const compareV = modelComparable.compare(modelComparable); - - // Then - expect(compareV).toEqual(0); - expect(isEqualSpy).not.toHaveBeenCalled(); - }); - - it('should verify will return -1 when status is different', () => { - // Given - const componentStateComparable = ComponentStateImpl.of({ - ...componentState, - status: LOADED - }); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(-1); - }); - - it('should verify will return -1 when task is different', () => { - // Given - const componentStateComparable = ComponentStateImpl.of({ - ...componentState, - task: 'patch_entity' - }); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(-1); - }); - - it('should verify will return 0 when navigationId is different', () => { - // Given - const routerStateComparable = RouterState.of(routerState.state, 90); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentState, routerStateComparable)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(0); - }); - - it('should verify will return -1 when error is different', () => { - // Given - const isEqualSpy = spyOn(CollectionsUtil, 'isEqual').and.callThrough(); - - const className = CollectionsUtil.generateRandomString(); - const publicName = CollectionsUtil.generateRandomString(); - const objectUUID = CollectionsUtil.generateObjectUUID(className); - const componentStateComparable = ComponentStateImpl.of({ - ...componentState, - errors: ErrorStoreImpl.of([ - { - error: new Error('new Error()'), - code: generateErrorCode(className, publicName, 'load', '503'), - objectUUID, - httpStatusCode: 503, - time: Date.now() - } - ]) - }); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(-1); - expect(isEqualSpy).not.toHaveBeenCalled(); - }); - - it('should verify will return 0 when data ref is same', () => { - // Given - const isEqualSpy = spyOn(CollectionsUtil, 'isEqual').and.callThrough(); - const componentStateComparable = ComponentStateImpl.of({ - ...componentState, - data: componentState.data - }); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(0); - expect(isEqualSpy).not.toHaveBeenCalled(); - }); - - it('should verify will return 0 when ref is different but content is same', () => { - // Given - const areMapsEqualSpy = spyOn(CollectionsUtil, 'areMapsEqual').and.callThrough(); - const componentStateComparable = ComponentStateImpl.of({ - ...componentState, - data: new Map([['countries', ['aCountry', 'bCountry', 'cCountry']]]) - }); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(0); - expect(areMapsEqualSpy).toHaveBeenCalledWith(componentState.data, componentStateComparable.data); - }); - - it('should verify will return -1 when ref is different and content is different', () => { - // Given - const areMapsEqualSpy = spyOn(CollectionsUtil, 'areMapsEqual').and.callThrough(); - const componentStateComparable = ComponentStateImpl.of({ - ...componentState, - data: new Map([ - ['countries', ['aCountry', 'bCountry', 'cCountry']], - ['users', ['aUser', 'bUser', 'cUser']] - ]) - }); - const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); - - // When - const compareV = modelComparable.compare(modelComparableDI); - - // Then - expect(compareV).toEqual(-1); - expect(areMapsEqualSpy).toHaveBeenCalledWith(componentState.data, componentStateComparable.data); - }); + // Then + expect(compareV).toEqual(0); + expect(isEqualSpy).not.toHaveBeenCalled(); + }); + + it("should verify will return 0 when ref is different but content is same", () => { + // Given + const areMapsEqualSpy = spyOn( + CollectionsUtil, + "areMapsEqual", + ).and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + data: new Map([ + ["countries", ["aCountry", "bCountry", "cCountry"]], + ]), }); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentStateComparable, routerState), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(0); + expect(areMapsEqualSpy).toHaveBeenCalledWith( + componentState.data, + componentStateComparable.data, + ); + }); + + it("should verify will return -1 when ref is different and content is different", () => { + // Given + const areMapsEqualSpy = spyOn( + CollectionsUtil, + "areMapsEqual", + ).and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + data: new Map([ + ["countries", ["aCountry", "bCountry", "cCountry"]], + ["users", ["aUser", "bUser", "cUser"]], + ]), + }); + const modelComparableDI = ComponentModelComparable.of( + ComponentModel.of(componentStateComparable, routerState), + ); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + expect(areMapsEqualSpy).toHaveBeenCalledWith( + componentState.data, + componentStateComparable.data, + ); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts index 18a5f0ac77..45def41a8c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts @@ -3,71 +3,83 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { Comparable, ComparableImpl } from '../../../common'; +import { Comparable, ComparableImpl } from "../../../common"; -import { AbstractComponentModel } from './component.model.interface'; +import { AbstractComponentModel } from "./component.model.interface"; export class ComponentModelComparable extends ComparableImpl { - /** - * ** Constructor. - */ - constructor(model: AbstractComponentModel) { - super(model); - } + /** + * ** Constructor. + */ + constructor(model: AbstractComponentModel) { + super(model); + } - /** - * ** Factory method. - */ - static override of(model: AbstractComponentModel): ComponentModelComparable { - return new ComponentModelComparable(model); - } + /** + * ** Factory method. + */ + static override of(model: AbstractComponentModel): ComponentModelComparable { + return new ComponentModelComparable(model); + } - /** - * @inheritDoc - */ - override compare(comparable: Comparable): number { - if (comparable instanceof ComponentModelComparable) { - return this._compareEquality(comparable); - } else { - return -1; - } + /** + * @inheritDoc + */ + override compare(comparable: Comparable): number { + if (comparable instanceof ComponentModelComparable) { + return this._compareEquality(comparable); + } else { + return -1; } + } - private _compareEquality(comparable: ComponentModelComparable): number { - if (!(this.value instanceof AbstractComponentModel)) { - return -1; - } - - if (!(comparable.value instanceof AbstractComponentModel)) { - return -1; - } + private _compareEquality(comparable: ComponentModelComparable): number { + if (!(this.value instanceof AbstractComponentModel)) { + return -1; + } - if (this.value === comparable.value) { - return 0; - } + if (!(comparable.value instanceof AbstractComponentModel)) { + return -1; + } - if (this.value.status !== comparable.value.status) { - return -1; - } + if (this.value === comparable.value) { + return 0; + } - if (this.value.getTask() !== comparable.value.getTask()) { - return -1; - } + if (this.value.status !== comparable.value.status) { + return -1; + } - if (!this.value.getComponentState().errors.equals(comparable.value.getComponentState().errors)) { - return -1; - } + if (this.value.getTask() !== comparable.value.getTask()) { + return -1; + } - if (this.value.getComponentState().data === comparable.value.getComponentState().data) { - return 0; - } + if ( + !this.value + .getComponentState() + .errors.equals(comparable.value.getComponentState().errors) + ) { + return -1; + } - if (CollectionsUtil.areMapsEqual(this.value.getComponentState().data, comparable.value.getComponentState().data)) { - return 0; - } + if ( + this.value.getComponentState().data === + comparable.value.getComponentState().data + ) { + return 0; + } - return -1; + if ( + CollectionsUtil.areMapsEqual( + this.value.getComponentState().data, + comparable.value.getComponentState().data, + ) + ) { + return 0; } + + return -1; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts index 4171eff445..2721dde2a7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts @@ -5,201 +5,201 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ApiPredicate, ErrorRecord } from '../../../common'; +import { ApiPredicate, ErrorRecord } from "../../../common"; -import { RouterState } from '../../router'; +import { RouterState } from "../../router"; -import { ComponentState, StatusType } from './state'; +import { ComponentState, StatusType } from "./state"; /** * ** Interface for Model for all Components. */ export abstract class AbstractComponentModel { - /** - * ** Return RouterState. - */ - abstract get routerState(): RouterState; - - /** - * ** Return Status from ComponentState. - */ - abstract get status(): StatusType; - - /** - * ** Return routePath from RouterState. - */ - abstract get routePath(): string; - - /** - * ** Return the ComponentState. - */ - abstract getComponentState(): ComponentState; - - /** - * ** Set Search query to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withSearch(search: string): this; - - /** - * ** Set Page to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withPage(page: number, size: number): this; - - /** - * ** Set Filter to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withFilter(filterPredicates: ApiPredicate[]): this; - - /** - * ** Set Request params to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withRequestParam(key: string, value: any): this; - - /** - * ** Set Data (bound to key identifier) to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withData(key: string, data: any): this; - - /** - * ** Set Task identifier to ComponentState and get Model reference again. - * - * - Should be set only after data comes from the API, action is asynchronous. - * - Don't set this property during action dispatch - * - *

    - * - Ready for method chaining. - *

    - */ - abstract withTask(taskIdentifier: string): this; - - /** - * ** Clear Task identifier from ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract clearTask(): this; - - /** - * ** Returns latest Task from ComponentState. - */ - abstract getTask(): string; - - /** - * ** Returns latest Task unique identifier from ComponentState. - */ - abstract getTaskUniqueIdentifier(): string; - - /** - * ** Set ErrorRecord to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withError(error: ErrorRecord): this; - - /** - * ** Clear Errors from ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract clearErrors(): this; - - /** - * ** Remove specific error codes from ErrorStore and altogether from ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract removeErrorCode(...errorCodes: string[]): this; - - /** - * ** Remove specific error code patterns from ErrorStore and altogether from ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract removeErrorCodePatterns(...errorCodePatterns: string[]): this; - - /** - * ** Set UiState for given identifier to ComponentState and get Model reference again. - *

    - * - Ready for method chaining. - *

    - */ - abstract withUiState(key: string, value: any): this; - - /** - * ** Returns UiState for given identifier. - */ - abstract getUiState(key: string): T; - - /** - * ** Set Component State status to IDLE. - *

    - * - Ready for method chaining. - *

    - */ - abstract withStatusIdle(): this; - - /** - * ** Set Component State status to LOADING. - *

    - * - Ready for method chaining. - *

    - */ - abstract withStatusLoading(): this; - - /** - * ** Set Component State status to LOADED. - *

    - * - Ready for method chaining. - *

    - */ - abstract withStatusLoaded(): this; - - /** - * ** Set Component State status to FAILED. - *

    - * - Ready for method chaining. - *

    - */ - abstract withStatusFailed(): this; - - /** - * ** Update Component State with Partial Component State patch. - *

    - * - Ready for method chaining. - *

    - */ - abstract updateComponentState(patchState: Partial): this; - - /** - * ** Prepare model for Component destroy and assign all fields to their targeted state. - *

    - * - Ready for method chaining. - *

    - */ - abstract prepareForDestroy(): this; - - /** - * ** Filter method that analyze if current Model is different from given one. - */ - abstract isModified(model: AbstractComponentModel): boolean; + /** + * ** Return RouterState. + */ + abstract get routerState(): RouterState; + + /** + * ** Return Status from ComponentState. + */ + abstract get status(): StatusType; + + /** + * ** Return routePath from RouterState. + */ + abstract get routePath(): string; + + /** + * ** Return the ComponentState. + */ + abstract getComponentState(): ComponentState; + + /** + * ** Set Search query to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withSearch(search: string): this; + + /** + * ** Set Page to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withPage(page: number, size: number): this; + + /** + * ** Set Filter to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withFilter(filterPredicates: ApiPredicate[]): this; + + /** + * ** Set Request params to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withRequestParam(key: string, value: any): this; + + /** + * ** Set Data (bound to key identifier) to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withData(key: string, data: any): this; + + /** + * ** Set Task identifier to ComponentState and get Model reference again. + * + * - Should be set only after data comes from the API, action is asynchronous. + * - Don't set this property during action dispatch + * + *

    + * - Ready for method chaining. + *

    + */ + abstract withTask(taskIdentifier: string): this; + + /** + * ** Clear Task identifier from ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract clearTask(): this; + + /** + * ** Returns latest Task from ComponentState. + */ + abstract getTask(): string; + + /** + * ** Returns latest Task unique identifier from ComponentState. + */ + abstract getTaskUniqueIdentifier(): string; + + /** + * ** Set ErrorRecord to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withError(error: ErrorRecord): this; + + /** + * ** Clear Errors from ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract clearErrors(): this; + + /** + * ** Remove specific error codes from ErrorStore and altogether from ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract removeErrorCode(...errorCodes: string[]): this; + + /** + * ** Remove specific error code patterns from ErrorStore and altogether from ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract removeErrorCodePatterns(...errorCodePatterns: string[]): this; + + /** + * ** Set UiState for given identifier to ComponentState and get Model reference again. + *

    + * - Ready for method chaining. + *

    + */ + abstract withUiState(key: string, value: any): this; + + /** + * ** Returns UiState for given identifier. + */ + abstract getUiState(key: string): T; + + /** + * ** Set Component State status to IDLE. + *

    + * - Ready for method chaining. + *

    + */ + abstract withStatusIdle(): this; + + /** + * ** Set Component State status to LOADING. + *

    + * - Ready for method chaining. + *

    + */ + abstract withStatusLoading(): this; + + /** + * ** Set Component State status to LOADED. + *

    + * - Ready for method chaining. + *

    + */ + abstract withStatusLoaded(): this; + + /** + * ** Set Component State status to FAILED. + *

    + * - Ready for method chaining. + *

    + */ + abstract withStatusFailed(): this; + + /** + * ** Update Component State with Partial Component State patch. + *

    + * - Ready for method chaining. + *

    + */ + abstract updateComponentState(patchState: Partial): this; + + /** + * ** Prepare model for Component destroy and assign all fields to their targeted state. + *

    + * - Ready for method chaining. + *

    + */ + abstract prepareForDestroy(): this; + + /** + * ** Filter method that analyze if current Model is different from given one. + */ + abstract isModified(model: AbstractComponentModel): boolean; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts index 0e0849edf7..dde697fa37 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts @@ -3,564 +3,670 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ASC, ErrorRecord, ErrorStore, generateErrorCode, RequestFilterImpl, RequestPageImpl } from '../../../common'; +import { + ASC, + ErrorRecord, + ErrorStore, + generateErrorCode, + RequestFilterImpl, + RequestPageImpl, +} from "../../../common"; + +import { CollectionsUtil } from "../../../utils"; + +import { CallFake } from "../../../unit-testing"; + +import { RouterState, RouteSegments, RouteState } from "../../router"; + +import { + ComponentState, + ComponentStateImpl, + FAILED, + IDLE, + INITIALIZED, + LOADED, + LOADING, +} from "./state"; + +import { ComponentModel } from "./component.model"; + +import { ComponentModelComparable } from "./component-model.comparable"; + +describe("ComponentModel", () => { + it("should verify instance is created", () => { + // When + const instance = new ComponentModel(null, null); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // Given + const componentState = { + id: "testId", + status: IDLE, + } as ComponentState; + const routerState = RouterState.of( + { + routeSegments: { + routePath: "domain/context", + }, + } as RouteState, + 9, + ); + + // When + const instance = ComponentModel.of(componentState, routerState); + + // Then + expect(instance).toBeInstanceOf(ComponentModel); + }); + }); + }); + }); + + describe("Getters/Setters::", () => { + let componentState: ComponentState; + let routerState: RouterState; + + beforeEach(() => { + componentState = { id: "testId", status: IDLE } as ComponentState; + routerState = RouterState.of( + { + routeSegments: { + routePath: "domain/context", + }, + } as RouteState, + 9, + ); + }); + + describe("|GET -> routerState|", () => { + it("should verify will return correct value", () => { + // Given + const model = ComponentModel.of(componentState, routerState); + + // When + const _routerState = model.routerState; + + // Then + expect(_routerState).toBe(routerState); + }); + }); -import { CollectionsUtil } from '../../../utils'; + describe("|GET -> status|", () => { + it("should verify will return correct value", () => { + // Given + const model = ComponentModel.of(componentState, routerState); -import { CallFake } from '../../../unit-testing'; + // When + const status = model.status; -import { RouterState, RouteSegments, RouteState } from '../../router'; + // Then + expect(status).toEqual(IDLE); + }); + }); + + describe("|GET -> routePath|", () => { + it("should verify will return correct value from componentState", () => { + // Given + componentState = { ...componentState, routePath: "entity/15" }; + const model = ComponentModel.of(componentState, routerState); -import { ComponentState, ComponentStateImpl, FAILED, IDLE, INITIALIZED, LOADED, LOADING } from './state'; + // When + const routePath = model.routePath; -import { ComponentModel } from './component.model'; + // Then + expect(routePath).toEqual("entity/15"); + }); -import { ComponentModelComparable } from './component-model.comparable'; + it("should verify will return correct value from routerState", () => { + // Given + const model = ComponentModel.of(componentState, routerState); -describe('ComponentModel', () => { - it('should verify instance is created', () => { // When - const instance = new ComponentModel(null, null); + const routePath = model.routePath; // Then - expect(instance).toBeDefined(); + expect(routePath).toEqual("domain/context"); + }); + }); + }); + + describe("Methods::", () => { + let componentState: ComponentState; + let routerState: RouterState; + let model: ComponentModel; + + beforeEach(() => { + componentState = ComponentStateImpl.of({ + id: "testId", + status: IDLE, + data: new Map([ + ["countries", ["aCountry", "bCountry", "cCountry"]], + ]), + }); + routerState = RouterState.of( + RouteState.of( + RouteSegments.of( + "entity/21", + { entityId: "21" }, + { search: "test-team" }, + RouteSegments.of("domain/context", {}, { search: "test-team" }), + ), + "domain/context/entity/21", + ), + 9, + ); + model = ComponentModel.of(componentState, routerState); }); - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // Given - const componentState = { id: 'testId', status: IDLE } as ComponentState; - const routerState = RouterState.of( - { - routeSegments: { - routePath: 'domain/context' - } - } as RouteState, - 9 - ); - - // When - const instance = ComponentModel.of(componentState, routerState); - - // Then - expect(instance).toBeInstanceOf(ComponentModel); - }); - }); - }); + describe("|getComponentState|", () => { + it("should verify will return ComponentState", () => { + // When + const _componentState = model.getComponentState(); + + // Then + expect(_componentState).toBe(componentState); + }); }); - describe('Getters/Setters::', () => { - let componentState: ComponentState; - let routerState: RouterState; - - beforeEach(() => { - componentState = { id: 'testId', status: IDLE } as ComponentState; - routerState = RouterState.of( - { - routeSegments: { - routePath: 'domain/context' - } - } as RouteState, - 9 - ); + describe("|withSearch|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); + + // When + const ref = model.withSearch("metadataSearchParam"); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ + search: "metadataSearchParam", }); + }); + }); - describe('|GET -> routerState|', () => { - it('should verify will return correct value', () => { - // Given - const model = ComponentModel.of(componentState, routerState); + describe("|withPage|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); - // When - const _routerState = model.routerState; + // When + const ref = model.withPage(6, 18); - // Then - expect(_routerState).toBe(routerState); - }); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ + page: RequestPageImpl.of(6, 18), }); + }); + }); - describe('|GET -> status|', () => { - it('should verify will return correct value', () => { - // Given - const model = ComponentModel.of(componentState, routerState); + describe("|withFilter|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); - // When - const status = model.status; + // When + const ref = model.withFilter([ + { pattern: "test*", property: "config.path", sort: ASC }, + ]); - // Then - expect(status).toEqual(IDLE); - }); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ + filter: RequestFilterImpl.of({ + pattern: "test*", + property: "config.path", + sort: ASC, + }), }); + }); + }); - describe('|GET -> routePath|', () => { - it('should verify will return correct value from componentState', () => { - // Given - componentState = { ...componentState, routePath: 'entity/15' }; - const model = ComponentModel.of(componentState, routerState); + describe("|withRequestParam|", () => { + it("should verify will set data to Request param map", () => { + // Given + const attributes = ["topBrand", "extraSize", "largeNumber"]; + const order = { sort: ASC, path: "config.user" }; - // When - const routePath = model.routePath; + // When + const ref = model + .withRequestParam("attributes", attributes) + .withRequestParam("order", order); - // Then - expect(routePath).toEqual('entity/15'); - }); + // Then + expect(ref).toBe(model); + expect(model.getComponentState().requestParams).toEqual( + new Map([ + ["attributes", [...attributes]], + ["order", { ...order }], + ]), + ); + }); + }); - it('should verify will return correct value from routerState', () => { - // Given - const model = ComponentModel.of(componentState, routerState); + describe("|withData|", () => { + it("should verify will set data to Data map", () => { + // Given + const users = ["aUser", "bUser", "cUser"]; - // When - const routePath = model.routePath; + // When + const ref = model.withData("users", users); - // Then - expect(routePath).toEqual('domain/context'); - }); - }); + // Then + expect(ref).toBe(model); + expect(model.getComponentState().data).toEqual( + new Map([ + ["countries", ["aCountry", "bCountry", "cCountry"]], + ["users", users], + ]), + ); + }); }); - describe('Methods::', () => { - let componentState: ComponentState; - let routerState: RouterState; - let model: ComponentModel; - - beforeEach(() => { - componentState = ComponentStateImpl.of({ - id: 'testId', - status: IDLE, - data: new Map([['countries', ['aCountry', 'bCountry', 'cCountry']]]) - }); - routerState = RouterState.of( - RouteState.of( - RouteSegments.of( - 'entity/21', - { entityId: '21' }, - { search: 'test-team' }, - RouteSegments.of('domain/context', {}, { search: 'test-team' }) - ), - 'domain/context/entity/21' - ), - 9 - ); - model = ComponentModel.of(componentState, routerState); - }); + describe("|withTask|", () => { + it("should verify will set Task to ComponentState", () => { + // Given + const task = "delete_entity"; - describe('|getComponentState|', () => { - it('should verify will return ComponentState', () => { - // When - const _componentState = model.getComponentState(); + // Then 1 + expect(model.getComponentState().task).not.toEqual(task); - // Then - expect(_componentState).toBe(componentState); - }); - }); - - describe('|withSearch|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + // When + const ref = model.withTask(task); - // When - const ref = model.withSearch('metadataSearchParam'); + // Then 2 + expect(ref).toBe(model); + expect(model.getComponentState().task).toEqual(task); + }); + }); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ search: 'metadataSearchParam' }); - }); - }); + describe("|clearTask|", () => { + it("should verify will invoke correct methods", () => { + // Given + const task = "delete_entity"; + model = ComponentModel.of( + ComponentStateImpl.of({ + ...componentState, + task, + }), + routerState, + ); + const spyUpdate = spyOn( + model, + "updateComponentState", + ).and.callThrough(); + + // Then 1 + expect(model.getComponentState().task).toEqual(task); - describe('|withPage|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + // When + const ref = model.clearTask(); - // When - const ref = model.withPage(6, 18); + // Then 2 + expect(ref).toBe(model); + expect(spyUpdate).toHaveBeenCalledWith({ task: null }); + expect(model.getComponentState().task).toEqual(null); + }); + }); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ page: RequestPageImpl.of(6, 18) }); - }); + describe("|getTask|", () => { + it("should verify will return Task from ComponentState", () => { + // Given + const task = "search_users"; + const taskIdentifier = `${task} __ ${new Date().toISOString()}`; + componentState = ComponentStateImpl.of({ + ...componentState, + task: taskIdentifier, }); + model = ComponentModel.of(componentState, routerState); - describe('|withFilter|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); - - // When - const ref = model.withFilter([{ pattern: 'test*', property: 'config.path', sort: ASC }]); - - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ - filter: RequestFilterImpl.of({ - pattern: 'test*', - property: 'config.path', - sort: ASC - }) - }); - }); - }); + // When + const res = model.getTask(); - describe('|withRequestParam|', () => { - it('should verify will set data to Request param map', () => { - // Given - const attributes = ['topBrand', 'extraSize', 'largeNumber']; - const order = { sort: ASC, path: 'config.user' }; - - // When - const ref = model.withRequestParam('attributes', attributes).withRequestParam('order', order); - - // Then - expect(ref).toBe(model); - expect(model.getComponentState().requestParams).toEqual( - new Map([ - ['attributes', [...attributes]], - ['order', { ...order }] - ]) - ); - }); - }); + // Then + expect(res).toBe(task); + }); + }); - describe('|withData|', () => { - it('should verify will set data to Data map', () => { - // Given - const users = ['aUser', 'bUser', 'cUser']; - - // When - const ref = model.withData('users', users); - - // Then - expect(ref).toBe(model); - expect(model.getComponentState().data).toEqual( - new Map([ - ['countries', ['aCountry', 'bCountry', 'cCountry']], - ['users', users] - ]) - ); - }); + describe("|getTaskUniqueIdentifier|", () => { + it("should verify will return Task in its unique form from ComponentState", () => { + // Given + const task = "search_users"; + const taskIdentifier = `${task} __ ${new Date().toISOString()}`; + componentState = ComponentStateImpl.of({ + ...componentState, + task: taskIdentifier, }); + model = ComponentModel.of(componentState, routerState); - describe('|withTask|', () => { - it('should verify will set Task to ComponentState', () => { - // Given - const task = 'delete_entity'; + // When + const res = model.getTaskUniqueIdentifier(); - // Then 1 - expect(model.getComponentState().task).not.toEqual(task); + // Then + expect(res).toBe(taskIdentifier); + }); + }); - // When - const ref = model.withTask(task); + describe("|withError|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const error = new Error("random error"); + const errorRecord: ErrorRecord = { + code: generateErrorCode( + "RandomXXService", + "Random-XX-Service", + "getLineageDataWithDepth", + "500", + ), + error, + objectUUID: CollectionsUtil.generateObjectUUID("RandomXXService"), + }; + const updateSpy = spyOn( + model.getComponentState().errors, + "record", + ).and.callThrough(); - // Then 2 - expect(ref).toBe(model); - expect(model.getComponentState().task).toEqual(task); - }); - }); + // When + const ref = model.withError(errorRecord); - describe('|clearTask|', () => { - it('should verify will invoke correct methods', () => { - // Given - const task = 'delete_entity'; - model = ComponentModel.of( - ComponentStateImpl.of({ - ...componentState, - task - }), - routerState - ); - const spyUpdate = spyOn(model, 'updateComponentState').and.callThrough(); - - // Then 1 - expect(model.getComponentState().task).toEqual(task); - - // When - const ref = model.clearTask(); - - // Then 2 - expect(ref).toBe(model); - expect(spyUpdate).toHaveBeenCalledWith({ task: null }); - expect(model.getComponentState().task).toEqual(null); - }); - }); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith(errorRecord); + }); + }); - describe('|getTask|', () => { - it('should verify will return Task from ComponentState', () => { - // Given - const task = 'search_users'; - const taskIdentifier = `${task} __ ${new Date().toISOString()}`; - componentState = ComponentStateImpl.of({ - ...componentState, - task: taskIdentifier - }); - model = ComponentModel.of(componentState, routerState); - - // When - const res = model.getTask(); - - // Then - expect(res).toBe(task); - }); - }); + describe("|clearErrors|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model.getComponentState().errors, + "clear", + ).and.callThrough(); - describe('|getTaskUniqueIdentifier|', () => { - it('should verify will return Task in its unique form from ComponentState', () => { - // Given - const task = 'search_users'; - const taskIdentifier = `${task} __ ${new Date().toISOString()}`; - componentState = ComponentStateImpl.of({ - ...componentState, - task: taskIdentifier - }); - model = ComponentModel.of(componentState, routerState); - - // When - const res = model.getTaskUniqueIdentifier(); - - // Then - expect(res).toBe(taskIdentifier); - }); - }); + // When + const ref = model.clearErrors(); - describe('|withError|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const error = new Error('random error'); - const errorRecord: ErrorRecord = { - code: generateErrorCode('RandomXXService', 'Random-XX-Service', 'getLineageDataWithDepth', '500'), - error, - objectUUID: CollectionsUtil.generateObjectUUID('RandomXXService') - }; - const updateSpy = spyOn(model.getComponentState().errors, 'record').and.callThrough(); - - // When - const ref = model.withError(errorRecord); - - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith(errorRecord); - }); - }); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalled(); + }); + }); - describe('|clearErrors|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model.getComponentState().errors, 'clear').and.callThrough(); + describe("|removeErrorCode|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const errorCodes: string[] = ["errorCode1", "errorCode3", "errorCode2"]; + const errorStoreStub = jasmine.createSpyObj( + "errorStoreStub", + ["removeCode"], + ); + const spyComponentState = spyOn( + model, + "getComponentState", + ).and.returnValue( + ComponentStateImpl.of({ + ...componentState, + errors: errorStoreStub, + }), + ); - // When - const ref = model.clearErrors(); + // When + const ref = model.removeErrorCode( + errorCodes[0], + errorCodes[1], + errorCodes[2], + ); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalled(); - }); - }); + // Then + expect(ref).toBe(model); + expect(spyComponentState).toHaveBeenCalled(); + expect(errorStoreStub.removeCode).toHaveBeenCalledWith( + errorCodes[0], + errorCodes[1], + errorCodes[2], + ); + }); + }); - describe('|removeErrorCode|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const errorCodes: string[] = ['errorCode1', 'errorCode3', 'errorCode2']; - const errorStoreStub = jasmine.createSpyObj('errorStoreStub', ['removeCode']); - const spyComponentState = spyOn(model, 'getComponentState').and.returnValue( - ComponentStateImpl.of({ - ...componentState, - errors: errorStoreStub - }) - ); - - // When - const ref = model.removeErrorCode(errorCodes[0], errorCodes[1], errorCodes[2]); - - // Then - expect(ref).toBe(model); - expect(spyComponentState).toHaveBeenCalled(); - expect(errorStoreStub.removeCode).toHaveBeenCalledWith(errorCodes[0], errorCodes[1], errorCodes[2]); - }); - }); + describe("|removeErrorCodePatterns|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const errorCodePatterns: string[] = [ + "errorCodePattern10", + "errorCodePattern30", + "errorCodePattern20", + ]; + const errorStoreStub = jasmine.createSpyObj( + "errorStoreStub", + ["removeCodePattern"], + ); + const spyComponentState = spyOn( + model, + "getComponentState", + ).and.returnValue( + ComponentStateImpl.of({ + ...componentState, + errors: errorStoreStub, + }), + ); - describe('|removeErrorCodePatterns|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const errorCodePatterns: string[] = ['errorCodePattern10', 'errorCodePattern30', 'errorCodePattern20']; - const errorStoreStub = jasmine.createSpyObj('errorStoreStub', ['removeCodePattern']); - const spyComponentState = spyOn(model, 'getComponentState').and.returnValue( - ComponentStateImpl.of({ - ...componentState, - errors: errorStoreStub - }) - ); - - // When - const ref = model.removeErrorCodePatterns(errorCodePatterns[0], errorCodePatterns[1], errorCodePatterns[2]); - - // Then - expect(ref).toBe(model); - expect(spyComponentState).toHaveBeenCalled(); - expect(errorStoreStub.removeCodePattern).toHaveBeenCalledWith( - errorCodePatterns[0], - errorCodePatterns[1], - errorCodePatterns[2] - ); - }); - }); + // When + const ref = model.removeErrorCodePatterns( + errorCodePatterns[0], + errorCodePatterns[1], + errorCodePatterns[2], + ); - describe('|withUiState|', () => { - it('should verify will set data to Data map', () => { - // Given - const btnState = { active: true }; + // Then + expect(ref).toBe(model); + expect(spyComponentState).toHaveBeenCalled(); + expect(errorStoreStub.removeCodePattern).toHaveBeenCalledWith( + errorCodePatterns[0], + errorCodePatterns[1], + errorCodePatterns[2], + ); + }); + }); - // When - const ref = model.withUiState('btnOk', btnState); + describe("|withUiState|", () => { + it("should verify will set data to Data map", () => { + // Given + const btnState = { active: true }; - // Then - expect(ref).toBe(model); - expect(model.getComponentState().uiState).toEqual(new Map([['btnOk', btnState]])); - }); - }); + // When + const ref = model.withUiState("btnOk", btnState); + + // Then + expect(ref).toBe(model); + expect(model.getComponentState().uiState).toEqual( + new Map([["btnOk", btnState]]), + ); + }); + }); - describe('|getUiState|', () => { - it('should verify will return uiState from ComponentState for given key', () => { - // Given - const btnState = { active: true }; - componentState = ComponentStateImpl.of({ - ...componentState, - uiState: new Map([['btnOk', btnState]]) - }); - model = ComponentModel.of(componentState, routerState); - - // When - const uiState = model.getUiState('btnOk'); - - // Then - expect(uiState).toBe(btnState); - }); + describe("|getUiState|", () => { + it("should verify will return uiState from ComponentState for given key", () => { + // Given + const btnState = { active: true }; + componentState = ComponentStateImpl.of({ + ...componentState, + uiState: new Map([["btnOk", btnState]]), }); + model = ComponentModel.of(componentState, routerState); - describe('|withStatusIdle|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + // When + const uiState = model.getUiState("btnOk"); - // When - const ref = model.withStatusIdle(); + // Then + expect(uiState).toBe(btnState); + }); + }); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ status: IDLE }); - }); - }); + describe("|withStatusIdle|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); - describe('|withStatusLoading|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + // When + const ref = model.withStatusIdle(); - // When - const ref = model.withStatusLoading(); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: IDLE }); + }); + }); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ status: LOADING }); - }); - }); + describe("|withStatusLoading|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); - describe('|withStatusLoaded|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + // When + const ref = model.withStatusLoading(); - // When - const ref = model.withStatusLoaded(); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: LOADING }); + }); + }); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ status: LOADED }); - }); - }); + describe("|withStatusLoaded|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); + + // When + const ref = model.withStatusLoaded(); - describe('|withStatusFailed|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: LOADED }); + }); + }); - // When - const ref = model.withStatusFailed(); + describe("|withStatusFailed|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const updateSpy = spyOn( + model, + "updateComponentState", + ).and.callThrough(); - // Then - expect(ref).toBe(model); - expect(updateSpy).toHaveBeenCalledWith({ status: FAILED }); - }); + // When + const ref = model.withStatusFailed(); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: FAILED }); + }); + }); + + describe("|updateComponentState|", () => { + it("should verify will update local ComponentState", () => { + // Given + const assertionComponentState = ComponentStateImpl.of({ + ...componentState, + status: INITIALIZED, + id: "test-component-1234", + search: "teamUSA*", + page: RequestPageImpl.of(9, 45), }); - describe('|updateComponentState|', () => { - it('should verify will update local ComponentState', () => { - // Given - const assertionComponentState = ComponentStateImpl.of({ - ...componentState, - status: INITIALIZED, - id: 'test-component-1234', - search: 'teamUSA*', - page: RequestPageImpl.of(9, 45) - }); - - // When - const ref = model.updateComponentState({ - status: INITIALIZED, - id: 'test-component-1234', - search: 'teamUSA*', - page: RequestPageImpl.of(9, 45) - }); - - // Then - expect(ref).toBe(model); - expect(model.getComponentState()).toEqual(assertionComponentState); - }); + // When + const ref = model.updateComponentState({ + status: INITIALIZED, + id: "test-component-1234", + search: "teamUSA*", + page: RequestPageImpl.of(9, 45), }); - describe('|prepareForDestroy|', () => { - it('should verify will invoke correct method with correct payload', () => { - // Given - const spyWithStatusIdle = spyOn(model, 'withStatusIdle').and.callFake(CallFake); - const spyUpdate = spyOn(model, 'updateComponentState').and.callThrough(); + // Then + expect(ref).toBe(model); + expect(model.getComponentState()).toEqual(assertionComponentState); + }); + }); - // When - const ref = model.prepareForDestroy(); + describe("|prepareForDestroy|", () => { + it("should verify will invoke correct method with correct payload", () => { + // Given + const spyWithStatusIdle = spyOn(model, "withStatusIdle").and.callFake( + CallFake, + ); + const spyUpdate = spyOn( + model, + "updateComponentState", + ).and.callThrough(); - // Then - expect(ref).toBe(model); - expect(spyWithStatusIdle).toHaveBeenCalled(); - expect(spyUpdate).toHaveBeenCalledWith({ errors: null }); - }); - }); + // When + const ref = model.prepareForDestroy(); - describe('|isModified|', () => { - it('should verify will invoke correct methods', () => { - // Given - const componentModelComparableStub = jasmine.createSpyObj('comparable', ['notEqual']); - componentModelComparableStub.notEqual.and.returnValue(false); - const factoryOfSpy = spyOn(ComponentModelComparable, 'of').and.returnValue(componentModelComparableStub); - - const comparableState = ComponentStateImpl.of({ - ...componentState, - data: new Map([ - ['countries', ['aCountry', 'bCountry', 'cCountry']], - ['users', ['aUser', 'bUser', 'cUser']] - ]) - }); - const comparableModel = ComponentModel.of(comparableState, routerState); - - // When - const isModified = model.isModified(comparableModel); - - // Then - expect(isModified).toBeFalse(); - expect(factoryOfSpy).toHaveBeenCalledTimes(2); - expect(factoryOfSpy.calls.argsFor(0)).toEqual([model]); - expect(factoryOfSpy.calls.argsFor(1)).toEqual([comparableModel]); - expect(componentModelComparableStub.notEqual).toHaveBeenCalledTimes(1); - expect(componentModelComparableStub.notEqual).toHaveBeenCalledWith(componentModelComparableStub); - }); + // Then + expect(ref).toBe(model); + expect(spyWithStatusIdle).toHaveBeenCalled(); + expect(spyUpdate).toHaveBeenCalledWith({ errors: null }); + }); + }); + + describe("|isModified|", () => { + it("should verify will invoke correct methods", () => { + // Given + const componentModelComparableStub = + jasmine.createSpyObj("comparable", [ + "notEqual", + ]); + componentModelComparableStub.notEqual.and.returnValue(false); + const factoryOfSpy = spyOn( + ComponentModelComparable, + "of", + ).and.returnValue(componentModelComparableStub); + + const comparableState = ComponentStateImpl.of({ + ...componentState, + data: new Map([ + ["countries", ["aCountry", "bCountry", "cCountry"]], + ["users", ["aUser", "bUser", "cUser"]], + ]), }); + const comparableModel = ComponentModel.of(comparableState, routerState); + + // When + const isModified = model.isModified(comparableModel); + + // Then + expect(isModified).toBeFalse(); + expect(factoryOfSpy).toHaveBeenCalledTimes(2); + expect(factoryOfSpy.calls.argsFor(0)).toEqual([model]); + expect(factoryOfSpy.calls.argsFor(1)).toEqual([comparableModel]); + expect(componentModelComparableStub.notEqual).toHaveBeenCalledTimes(1); + expect(componentModelComparableStub.notEqual).toHaveBeenCalledWith( + componentModelComparableStub, + ); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts index 394a18260f..034ef97354 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts @@ -5,270 +5,289 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ApiPredicate, ErrorRecord, extractTaskFromIdentifier, RequestFilterImpl, RequestPageImpl } from '../../../common'; - -import { RouterState } from '../../router'; - -import { ComponentState, ComponentStateImpl, FAILED, IDLE, LOADED, LOADING, StatusType } from './state'; - -import { ComponentModelComparable } from './component-model.comparable'; - -import { AbstractComponentModel } from './component.model.interface'; +import { + ApiPredicate, + ErrorRecord, + extractTaskFromIdentifier, + RequestFilterImpl, + RequestPageImpl, +} from "../../../common"; + +import { RouterState } from "../../router"; + +import { + ComponentState, + ComponentStateImpl, + FAILED, + IDLE, + LOADED, + LOADING, + StatusType, +} from "./state"; + +import { ComponentModelComparable } from "./component-model.comparable"; + +import { AbstractComponentModel } from "./component.model.interface"; /** * ** Generic Model for all Components. */ export class ComponentModel extends AbstractComponentModel { - /** - * ** Constructor. - */ - constructor( - protected _componentState: ComponentState, - protected _routerState: RouterState - ) { - super(); - } - - /** - * ** Factory method. - */ - static of(componentState: ComponentState, routerState: RouterState) { - return new ComponentModel(componentState, routerState); - } - - /** - * @inheritDoc - */ - get routerState(): RouterState { - return this._routerState; - } - - /** - * @inheritDoc - */ - get status(): StatusType { - return this.getComponentState().status; - } - - /** - * @inheritDoc - */ - get routePath(): string { - return this.getComponentState().routePath || this.routerState.state.routeSegments.routePath; - } - - /** - * ** Reference to previous model for comparison only. - */ - readonly previousModel: Readonly; - - /** - * @inheritDoc - */ - getComponentState(): ComponentState { - return this._componentState; - } - - /** - * @inheritDoc - */ - withSearch(search: string) { - this.updateComponentState({ - search - }); - - return this; - } - - /** - * @inheritDoc - */ - withPage(page: number, size: number) { - this.updateComponentState({ - page: RequestPageImpl.of(page, size) - }); - - return this; - } - - /** - * @inheritDoc - */ - withFilter(filterPredicates: ApiPredicate[]) { - this.updateComponentState({ - filter: RequestFilterImpl.of(...filterPredicates) - }); - - return this; - } - - /** - * @inheritDoc - */ - withRequestParam(key: string, value: any) { - this.getComponentState().requestParams.set(key, value); - - return this; - } - - /** - * @inheritDoc - */ - withData(key: string, data: any) { - this.getComponentState().data.set(key, data); - - return this; - } - - /** - * @inheritDoc - */ - withTask(taskIdentifier: string) { - this.updateComponentState({ task: taskIdentifier }); - - return this; - } - - /** - * @inheritDoc - */ - clearTask() { - this.updateComponentState({ task: null }); - - return this; - } - - /** - * @inheritDoc - */ - getTask(): string { - return extractTaskFromIdentifier(this.getComponentState().task); - } - - /** - * @inheritDoc - */ - getTaskUniqueIdentifier(): string { - return this.getComponentState().task; - } - - /** - * @inheritDoc - */ - withError(errorRecord: ErrorRecord) { - this.getComponentState().errors.record(errorRecord); - - return this; - } - - /** - * @inheritDoc - */ - clearErrors() { - this.getComponentState().errors.clear(); - - return this; - } - - /** - * @inheritDoc - */ - removeErrorCode(...errorCodes: string[]): this { - this.getComponentState().errors.removeCode(...errorCodes); - - return this; - } - - /** - * @inheritDoc - */ - removeErrorCodePatterns(...errorCodePatterns: string[]): this { - this.getComponentState().errors.removeCodePattern(...errorCodePatterns); - - return this; - } - - /** - * @inheritDoc - */ - withUiState(key: string, value: any) { - this.getComponentState().uiState.set(key, value); - - return this; - } - - /** - * @inheritDoc - */ - getUiState(key: string): T { - return this.getComponentState().uiState.get(key) as T; - } - - /** - * @inheritDoc - */ - withStatusIdle() { - this.updateComponentState({ status: IDLE }); - - return this; - } - - /** - * @inheritDoc - */ - withStatusLoading() { - this.updateComponentState({ status: LOADING }); - - return this; - } - - /** - * @inheritDoc - */ - withStatusLoaded() { - this.updateComponentState({ status: LOADED }); - - return this; - } - - /** - * @inheritDoc - */ - withStatusFailed() { - this.updateComponentState({ status: FAILED }); - - return this; - } - - /** - * @inheritDoc - */ - updateComponentState(patchState: Partial) { - this._componentState = ComponentStateImpl.of({ - ...this.getComponentState(), - ...patchState - }); - - return this; - } - - /** - * @inheritDoc - */ - prepareForDestroy() { - this.withStatusIdle(); - - this.updateComponentState({ - errors: null - }); - - return this; - } - - /** - * @inheritDoc - */ - isModified(model: ComponentModel): boolean { - return ComponentModelComparable.of(this).notEqual(ComponentModelComparable.of(model)); - } + /** + * ** Constructor. + */ + constructor( + protected _componentState: ComponentState, + protected _routerState: RouterState, + ) { + super(); + } + + /** + * ** Factory method. + */ + static of(componentState: ComponentState, routerState: RouterState) { + return new ComponentModel(componentState, routerState); + } + + /** + * @inheritDoc + */ + get routerState(): RouterState { + return this._routerState; + } + + /** + * @inheritDoc + */ + get status(): StatusType { + return this.getComponentState().status; + } + + /** + * @inheritDoc + */ + get routePath(): string { + return ( + this.getComponentState().routePath || + this.routerState.state.routeSegments.routePath + ); + } + + /** + * ** Reference to previous model for comparison only. + */ + readonly previousModel: Readonly; + + /** + * @inheritDoc + */ + getComponentState(): ComponentState { + return this._componentState; + } + + /** + * @inheritDoc + */ + withSearch(search: string) { + this.updateComponentState({ + search, + }); + + return this; + } + + /** + * @inheritDoc + */ + withPage(page: number, size: number) { + this.updateComponentState({ + page: RequestPageImpl.of(page, size), + }); + + return this; + } + + /** + * @inheritDoc + */ + withFilter(filterPredicates: ApiPredicate[]) { + this.updateComponentState({ + filter: RequestFilterImpl.of(...filterPredicates), + }); + + return this; + } + + /** + * @inheritDoc + */ + withRequestParam(key: string, value: any) { + this.getComponentState().requestParams.set(key, value); + + return this; + } + + /** + * @inheritDoc + */ + withData(key: string, data: any) { + this.getComponentState().data.set(key, data); + + return this; + } + + /** + * @inheritDoc + */ + withTask(taskIdentifier: string) { + this.updateComponentState({ task: taskIdentifier }); + + return this; + } + + /** + * @inheritDoc + */ + clearTask() { + this.updateComponentState({ task: null }); + + return this; + } + + /** + * @inheritDoc + */ + getTask(): string { + return extractTaskFromIdentifier(this.getComponentState().task); + } + + /** + * @inheritDoc + */ + getTaskUniqueIdentifier(): string { + return this.getComponentState().task; + } + + /** + * @inheritDoc + */ + withError(errorRecord: ErrorRecord) { + this.getComponentState().errors.record(errorRecord); + + return this; + } + + /** + * @inheritDoc + */ + clearErrors() { + this.getComponentState().errors.clear(); + + return this; + } + + /** + * @inheritDoc + */ + removeErrorCode(...errorCodes: string[]): this { + this.getComponentState().errors.removeCode(...errorCodes); + + return this; + } + + /** + * @inheritDoc + */ + removeErrorCodePatterns(...errorCodePatterns: string[]): this { + this.getComponentState().errors.removeCodePattern(...errorCodePatterns); + + return this; + } + + /** + * @inheritDoc + */ + withUiState(key: string, value: any) { + this.getComponentState().uiState.set(key, value); + + return this; + } + + /** + * @inheritDoc + */ + getUiState(key: string): T { + return this.getComponentState().uiState.get(key) as T; + } + + /** + * @inheritDoc + */ + withStatusIdle() { + this.updateComponentState({ status: IDLE }); + + return this; + } + + /** + * @inheritDoc + */ + withStatusLoading() { + this.updateComponentState({ status: LOADING }); + + return this; + } + + /** + * @inheritDoc + */ + withStatusLoaded() { + this.updateComponentState({ status: LOADED }); + + return this; + } + + /** + * @inheritDoc + */ + withStatusFailed() { + this.updateComponentState({ status: FAILED }); + + return this; + } + + /** + * @inheritDoc + */ + updateComponentState(patchState: Partial) { + this._componentState = ComponentStateImpl.of({ + ...this.getComponentState(), + ...patchState, + }); + + return this; + } + + /** + * @inheritDoc + */ + prepareForDestroy() { + this.withStatusIdle(); + + this.updateComponentState({ + errors: null, + }); + + return this; + } + + /** + * @inheritDoc + */ + isModified(model: ComponentModel): boolean { + return ComponentModelComparable.of(this).notEqual( + ComponentModelComparable.of(model), + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts index 97e2bacf97..f8b6fbd1d9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './state'; -export * from './component.model.interface'; -export * from './component.model'; +export * from "./state"; +export * from "./component.model.interface"; +export * from "./component.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts index 542292937d..b2c59fa739 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts @@ -3,302 +3,368 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; import { - ApiPredicate, - ASC, - ErrorRecord, - generateErrorCode, - RequestFilterImpl, - RequestOrderImpl, - RequestPageImpl -} from '../../../../common'; - -import { ErrorStoreImpl } from '../../../error'; - -import { IDLE, LOADED } from './component-status.model'; - -import { ComponentState, ComponentStateImpl, LiteralComponentState } from './component-state.model'; - -describe('ComponentStateImpl', () => { - let partialStateMock: Partial; - let stateMock: ComponentStateImpl; - let literalStateMock: LiteralComponentState; - - beforeEach(() => { - const apiPredicate1: ApiPredicate = { - sort: ASC, - property: 'test.property.10', - pattern: 'test.pattern.20' - }; - const apiPredicate2: ApiPredicate = { - sort: ASC, - property: 'test.property.30', - pattern: 'test.pattern.40' - }; + ApiPredicate, + ASC, + ErrorRecord, + generateErrorCode, + RequestFilterImpl, + RequestOrderImpl, + RequestPageImpl, +} from "../../../../common"; - const uiState1 = { order: 'ASC' }; - const uiState2 = [1, 2, 3]; - - const data1 = { data: { name: 'aName' } }; - const data2 = { content: { page: { size: 10, page: 2 } } }; - - const dateNowISO = new Date().toISOString(); - - const errorRecords: ErrorRecord[] = [ - { - code: generateErrorCode('RandomClassName', 'RandomPublicName', 'randomMethod', 'unknown'), - objectUUID: CollectionsUtil.generateObjectUUID('RandomClassName'), - error: new Error('Random error!'), - time: CollectionsUtil.dateNow() - } - ]; - - partialStateMock = { - id: 'testId', - status: LOADED, - navigationId: 11, - errors: ErrorStoreImpl.of(errorRecords.map((r) => ({ ...r }))), - search: 'testSearch', - routePath: 'domain/context/entity/10', - routePathSegments: ['domain/context', 'entity/10'], - page: new RequestPageImpl(5, 10), - order: new RequestOrderImpl(apiPredicate1), - filter: new RequestFilterImpl(apiPredicate2), - requestParams: new Map([ - ['test_param.1', apiPredicate1], - ['test_param.2', apiPredicate2] - ]), - task: `delete_user __ ${dateNowISO}`, - uiState: new Map([ - ['test_uiState.1', uiState1], - ['test_uiState.2', uiState2] - ]), - data: new Map([ - ['test_data.1', data1], - ['test_data.2', data2] - ]) - }; +import { ErrorStoreImpl } from "../../../error"; - stateMock = new ComponentStateImpl(partialStateMock); - - literalStateMock = { - id: 'testId', - status: LOADED, - navigationId: 11, - errors: errorRecords.map((r) => ({ ...r })), - search: 'testSearch', - routePath: 'domain/context/entity/10', - routePathSegments: ['domain/context', 'entity/10'], - page: { pageNumber: 5, pageSize: 10 }, - order: [apiPredicate1], - filter: [apiPredicate2], - requestParams: { - 'test_param.1': apiPredicate1, - 'test_param.2': apiPredicate2 - }, - task: `delete_user __ ${dateNowISO}`, - uiState: { - 'test_uiState.1': uiState1, - 'test_uiState.2': uiState2 - }, - data: { - 'test_data.1': data1, - 'test_data.2': data2 - } - }; +import { IDLE, LOADED } from "./component-status.model"; + +import { + ComponentState, + ComponentStateImpl, + LiteralComponentState, +} from "./component-state.model"; + +describe("ComponentStateImpl", () => { + let partialStateMock: Partial; + let stateMock: ComponentStateImpl; + let literalStateMock: LiteralComponentState; + + beforeEach(() => { + const apiPredicate1: ApiPredicate = { + sort: ASC, + property: "test.property.10", + pattern: "test.pattern.20", + }; + const apiPredicate2: ApiPredicate = { + sort: ASC, + property: "test.property.30", + pattern: "test.pattern.40", + }; + + const uiState1 = { order: "ASC" }; + const uiState2 = [1, 2, 3]; + + const data1 = { data: { name: "aName" } }; + const data2 = { content: { page: { size: 10, page: 2 } } }; + + const dateNowISO = new Date().toISOString(); + + const errorRecords: ErrorRecord[] = [ + { + code: generateErrorCode( + "RandomClassName", + "RandomPublicName", + "randomMethod", + "unknown", + ), + objectUUID: CollectionsUtil.generateObjectUUID("RandomClassName"), + error: new Error("Random error!"), + time: CollectionsUtil.dateNow(), + }, + ]; + + partialStateMock = { + id: "testId", + status: LOADED, + navigationId: 11, + errors: ErrorStoreImpl.of(errorRecords.map((r) => ({ ...r }))), + search: "testSearch", + routePath: "domain/context/entity/10", + routePathSegments: ["domain/context", "entity/10"], + page: new RequestPageImpl(5, 10), + order: new RequestOrderImpl(apiPredicate1), + filter: new RequestFilterImpl(apiPredicate2), + requestParams: new Map([ + ["test_param.1", apiPredicate1], + ["test_param.2", apiPredicate2], + ]), + task: `delete_user __ ${dateNowISO}`, + uiState: new Map([ + ["test_uiState.1", uiState1], + ["test_uiState.2", uiState2], + ]), + data: new Map([ + ["test_data.1", data1], + ["test_data.2", data2], + ]), + }; + + stateMock = new ComponentStateImpl(partialStateMock); + + literalStateMock = { + id: "testId", + status: LOADED, + navigationId: 11, + errors: errorRecords.map((r) => ({ ...r })), + search: "testSearch", + routePath: "domain/context/entity/10", + routePathSegments: ["domain/context", "entity/10"], + page: { pageNumber: 5, pageSize: 10 }, + order: [apiPredicate1], + filter: [apiPredicate2], + requestParams: { + "test_param.1": apiPredicate1, + "test_param.2": apiPredicate2, + }, + task: `delete_user __ ${dateNowISO}`, + uiState: { + "test_uiState.1": uiState1, + "test_uiState.2": uiState2, + }, + data: { + "test_data.1": data1, + "test_data.2": data2, + }, + }; + }); + + it("should verify instance is created", () => { + // When + const instance = new ComponentStateImpl({}); + + // Then + expect(instance).toBeDefined(); + }); + + it("should verify provided value will be correctly assigned", () => { + // When + const instance = new ComponentStateImpl(partialStateMock); + + // Then + expect(instance.id).toEqual(partialStateMock.id); + expect(instance.status).toEqual(partialStateMock.status); + expect(instance.navigationId).toEqual(partialStateMock.navigationId); + expect(instance.errors).toEqual(partialStateMock.errors); + expect(instance.search).toEqual(partialStateMock.search); + expect(instance.routePath).toEqual(partialStateMock.routePath); + expect(instance.routePathSegments).toBe(partialStateMock.routePathSegments); + expect(instance.page).toBe(partialStateMock.page); + expect(instance.order).toBe(partialStateMock.order); + expect(instance.filter).toBe(partialStateMock.filter); + expect(instance.requestParams).toBe(partialStateMock.requestParams); + expect(instance.task).toEqual(partialStateMock.task); + expect(instance.uiState).toBe(partialStateMock.uiState); + expect(instance.data).toBe(partialStateMock.data); + }); + + it("should verify will correctly assign default values", () => { + // When + const instances: ComponentState[] = [ + new ComponentStateImpl({}), + new ComponentStateImpl(undefined), + ]; + + // Then + for (const instance of instances) { + expect(instance).toBeDefined(); + expect(instance.id).toBeUndefined(); + expect(instance.status).toEqual(IDLE); + expect(instance.navigationId).toEqual(null); + expect(instance.routePath).toBeUndefined(); + expect(instance.routePathSegments).toEqual([]); + expect(instance.search).toEqual(""); + expect(instance.page).toEqual(RequestPageImpl.empty()); + expect(instance.order).toEqual(RequestOrderImpl.empty()); + expect(instance.filter).toEqual(RequestFilterImpl.empty()); + expect(instance.requestParams).toBeInstanceOf(Map); + expect(instance.errors).toEqual(ErrorStoreImpl.empty()); + expect(instance.task).toEqual(null); + expect(instance.data).toBeInstanceOf(Map); + expect(instance.uiState).toBeInstanceOf(Map); + } + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = ComponentStateImpl.of(partialStateMock); + + // Then + expect(instance.id).toEqual(partialStateMock.id); + expect(instance.status).toEqual(partialStateMock.status); + expect(instance.navigationId).toEqual(partialStateMock.navigationId); + expect(instance.errors).toEqual(partialStateMock.errors); + expect(instance.search).toEqual(partialStateMock.search); + expect(instance.routePath).toEqual(partialStateMock.routePath); + expect(instance.routePathSegments).toBe( + partialStateMock.routePathSegments, + ); + expect(instance.page).toBe(partialStateMock.page); + expect(instance.order).toBe(partialStateMock.order); + expect(instance.filter).toBe(partialStateMock.filter); + expect(instance.requestParams).toBe(partialStateMock.requestParams); + expect(instance.task).toEqual(partialStateMock.task); + expect(instance.uiState).toBe(partialStateMock.uiState); + expect(instance.data).toBe(partialStateMock.data); + }); + }); + + describe("|fromLiteralComponentState|", () => { + it("should verify will create instance of ComponentStateImpl from LiteralComponentState", () => { + // When + const instance = + ComponentStateImpl.fromLiteralComponentState(literalStateMock); + + // Then + expect(instance).toEqual(stateMock); + expect(instance.order.criteria[0]).toBe(stateMock.order.criteria[0]); + expect(instance.filter.criteria[0]).toBe( + stateMock.filter.criteria[0], + ); + expect(instance.requestParams.get("test_param.1")).toBe( + stateMock.requestParams.get("test_param.1"), + ); + expect(instance.requestParams.get("test_param.2")).toBe( + stateMock.requestParams.get("test_param.2"), + ); + expect(instance.uiState.get("test_uiState.1")).toBe( + stateMock.uiState.get("test_uiState.1"), + ); + expect(instance.uiState.get("test_uiState.2")).toBe( + stateMock.uiState.get("test_uiState.2"), + ); + expect(instance.data.get("test_data.1")).toBe( + stateMock.data.get("test_data.1"), + ); + expect(instance.data.get("test_data.2")).toBe( + stateMock.data.get("test_data.2"), + ); + }); + }); + + describe("|cloneDeepLiteral|", () => { + it("should verify will create deep clone from LiteralComponentState", () => { + // When + const instance = + ComponentStateImpl.cloneDeepLiteral(literalStateMock); + + // Then + expect(instance).toEqual(literalStateMock); + expect(instance.order[0]).not.toBe(literalStateMock.order[0]); + expect(instance.filter[0]).not.toBe(literalStateMock.filter[0]); + expect(instance.requestParams["test_param.1"]).not.toBe( + literalStateMock.requestParams["test_param.1"], + ); + expect(instance.requestParams["test_param.2"]).not.toBe( + literalStateMock.requestParams["test_param.2"], + ); + expect(instance.uiState["test_uiState.1"]).not.toBe( + literalStateMock.uiState["test_uiState.1"], + ); + expect(instance.uiState["test_uiState.2"]).not.toBe( + literalStateMock.uiState["test_uiState.2"], + ); + expect(instance.data["test_data.1"]).not.toBe( + literalStateMock.data["test_data.1"], + ); + expect(instance.data["test_data.2"]).not.toBe( + literalStateMock.data["test_data.2"], + ); + }); + }); }); + }); - it('should verify instance is created', () => { + describe("Methods::", () => { + describe("|toLiteral|", () => { + it("should verify will create LiteralComponentState", () => { // When - const instance = new ComponentStateImpl({}); + const instance = stateMock.toLiteral(); // Then - expect(instance).toBeDefined(); + expect(instance).toEqual(literalStateMock); + expect(instance.order[0]).toBe(literalStateMock.order[0]); + expect(instance.filter[0]).toBe(literalStateMock.filter[0]); + expect(instance.requestParams["test_param.1"]).toBe( + literalStateMock.requestParams["test_param.1"], + ); + expect(instance.requestParams["test_param.2"]).toBe( + literalStateMock.requestParams["test_param.2"], + ); + expect(instance.uiState["test_uiState.1"]).toBe( + literalStateMock.uiState["test_uiState.1"], + ); + expect(instance.uiState["test_uiState.2"]).toBe( + literalStateMock.uiState["test_uiState.2"], + ); + expect(instance.data["test_data.1"]).toBe( + literalStateMock.data["test_data.1"], + ); + expect(instance.data["test_data.2"]).toBe( + literalStateMock.data["test_data.2"], + ); + }); }); - it('should verify provided value will be correctly assigned', () => { + describe("|toLiteralCloneDeep|", () => { + it("should verify will create LiteralComponentState deep cloned", () => { // When - const instance = new ComponentStateImpl(partialStateMock); + const instance = stateMock.toLiteralCloneDeep(); // Then - expect(instance.id).toEqual(partialStateMock.id); - expect(instance.status).toEqual(partialStateMock.status); - expect(instance.navigationId).toEqual(partialStateMock.navigationId); - expect(instance.errors).toEqual(partialStateMock.errors); - expect(instance.search).toEqual(partialStateMock.search); - expect(instance.routePath).toEqual(partialStateMock.routePath); - expect(instance.routePathSegments).toBe(partialStateMock.routePathSegments); - expect(instance.page).toBe(partialStateMock.page); - expect(instance.order).toBe(partialStateMock.order); - expect(instance.filter).toBe(partialStateMock.filter); - expect(instance.requestParams).toBe(partialStateMock.requestParams); - expect(instance.task).toEqual(partialStateMock.task); - expect(instance.uiState).toBe(partialStateMock.uiState); - expect(instance.data).toBe(partialStateMock.data); + expect(instance).toEqual(literalStateMock); + expect(instance.order[0]).not.toBe(literalStateMock.order[0]); + expect(instance.filter[0]).not.toBe(literalStateMock.filter[0]); + expect(instance.requestParams["test_param.1"]).not.toBe( + literalStateMock.requestParams["test_param.1"], + ); + expect(instance.requestParams["test_param.2"]).not.toBe( + literalStateMock.requestParams["test_param.2"], + ); + expect(instance.uiState["test_uiState.1"]).not.toBe( + literalStateMock.uiState["test_uiState.1"], + ); + expect(instance.uiState["test_uiState.2"]).not.toBe( + literalStateMock.uiState["test_uiState.2"], + ); + expect(instance.data["test_data.1"]).not.toBe( + literalStateMock.data["test_data.1"], + ); + expect(instance.data["test_data.2"]).not.toBe( + literalStateMock.data["test_data.2"], + ); + }); }); - it('should verify will correctly assign default values', () => { + describe("|copy|", () => { + it("should verify will create copy from ComponentStateImpl", () => { // When - const instances: ComponentState[] = [new ComponentStateImpl({}), new ComponentStateImpl(undefined)]; + const instance = stateMock.copy(); // Then - for (const instance of instances) { - expect(instance).toBeDefined(); - expect(instance.id).toBeUndefined(); - expect(instance.status).toEqual(IDLE); - expect(instance.navigationId).toEqual(null); - expect(instance.routePath).toBeUndefined(); - expect(instance.routePathSegments).toEqual([]); - expect(instance.search).toEqual(''); - expect(instance.page).toEqual(RequestPageImpl.empty()); - expect(instance.order).toEqual(RequestOrderImpl.empty()); - expect(instance.filter).toEqual(RequestFilterImpl.empty()); - expect(instance.requestParams).toBeInstanceOf(Map); - expect(instance.errors).toEqual(ErrorStoreImpl.empty()); - expect(instance.task).toEqual(null); - expect(instance.data).toBeInstanceOf(Map); - expect(instance.uiState).toBeInstanceOf(Map); - } - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentStateImpl.of(partialStateMock); - - // Then - expect(instance.id).toEqual(partialStateMock.id); - expect(instance.status).toEqual(partialStateMock.status); - expect(instance.navigationId).toEqual(partialStateMock.navigationId); - expect(instance.errors).toEqual(partialStateMock.errors); - expect(instance.search).toEqual(partialStateMock.search); - expect(instance.routePath).toEqual(partialStateMock.routePath); - expect(instance.routePathSegments).toBe(partialStateMock.routePathSegments); - expect(instance.page).toBe(partialStateMock.page); - expect(instance.order).toBe(partialStateMock.order); - expect(instance.filter).toBe(partialStateMock.filter); - expect(instance.requestParams).toBe(partialStateMock.requestParams); - expect(instance.task).toEqual(partialStateMock.task); - expect(instance.uiState).toBe(partialStateMock.uiState); - expect(instance.data).toBe(partialStateMock.data); - }); - }); - - describe('|fromLiteralComponentState|', () => { - it('should verify will create instance of ComponentStateImpl from LiteralComponentState', () => { - // When - const instance = ComponentStateImpl.fromLiteralComponentState(literalStateMock); - - // Then - expect(instance).toEqual(stateMock); - expect(instance.order.criteria[0]).toBe(stateMock.order.criteria[0]); - expect(instance.filter.criteria[0]).toBe(stateMock.filter.criteria[0]); - expect(instance.requestParams.get('test_param.1')).toBe(stateMock.requestParams.get('test_param.1')); - expect(instance.requestParams.get('test_param.2')).toBe(stateMock.requestParams.get('test_param.2')); - expect(instance.uiState.get('test_uiState.1')).toBe(stateMock.uiState.get('test_uiState.1')); - expect(instance.uiState.get('test_uiState.2')).toBe(stateMock.uiState.get('test_uiState.2')); - expect(instance.data.get('test_data.1')).toBe(stateMock.data.get('test_data.1')); - expect(instance.data.get('test_data.2')).toBe(stateMock.data.get('test_data.2')); - }); - }); - - describe('|cloneDeepLiteral|', () => { - it('should verify will create deep clone from LiteralComponentState', () => { - // When - const instance = ComponentStateImpl.cloneDeepLiteral(literalStateMock); - - // Then - expect(instance).toEqual(literalStateMock); - expect(instance.order[0]).not.toBe(literalStateMock.order[0]); - expect(instance.filter[0]).not.toBe(literalStateMock.filter[0]); - expect(instance.requestParams['test_param.1']).not.toBe(literalStateMock.requestParams['test_param.1']); - expect(instance.requestParams['test_param.2']).not.toBe(literalStateMock.requestParams['test_param.2']); - expect(instance.uiState['test_uiState.1']).not.toBe(literalStateMock.uiState['test_uiState.1']); - expect(instance.uiState['test_uiState.2']).not.toBe(literalStateMock.uiState['test_uiState.2']); - expect(instance.data['test_data.1']).not.toBe(literalStateMock.data['test_data.1']); - expect(instance.data['test_data.2']).not.toBe(literalStateMock.data['test_data.2']); - }); - }); - }); - }); - - describe('Methods::', () => { - describe('|toLiteral|', () => { - it('should verify will create LiteralComponentState', () => { - // When - const instance = stateMock.toLiteral(); - - // Then - expect(instance).toEqual(literalStateMock); - expect(instance.order[0]).toBe(literalStateMock.order[0]); - expect(instance.filter[0]).toBe(literalStateMock.filter[0]); - expect(instance.requestParams['test_param.1']).toBe(literalStateMock.requestParams['test_param.1']); - expect(instance.requestParams['test_param.2']).toBe(literalStateMock.requestParams['test_param.2']); - expect(instance.uiState['test_uiState.1']).toBe(literalStateMock.uiState['test_uiState.1']); - expect(instance.uiState['test_uiState.2']).toBe(literalStateMock.uiState['test_uiState.2']); - expect(instance.data['test_data.1']).toBe(literalStateMock.data['test_data.1']); - expect(instance.data['test_data.2']).toBe(literalStateMock.data['test_data.2']); - }); - }); + expect(instance).not.toBe(stateMock); + expect(instance).toEqual(stateMock); + }); + + it("should verify will merge provided State on top of original and return instance", () => { + // Given + const partialState: Partial = { + id: "newId", + status: LOADED, + search: "randomSearch", + routePath: "domain/context/entity/20", + routePathSegments: ["domain/context", "entity/20"], + data: new Map(), + uiState: new Map(), + }; - describe('|toLiteralCloneDeep|', () => { - it('should verify will create LiteralComponentState deep cloned', () => { - // When - const instance = stateMock.toLiteralCloneDeep(); - - // Then - expect(instance).toEqual(literalStateMock); - expect(instance.order[0]).not.toBe(literalStateMock.order[0]); - expect(instance.filter[0]).not.toBe(literalStateMock.filter[0]); - expect(instance.requestParams['test_param.1']).not.toBe(literalStateMock.requestParams['test_param.1']); - expect(instance.requestParams['test_param.2']).not.toBe(literalStateMock.requestParams['test_param.2']); - expect(instance.uiState['test_uiState.1']).not.toBe(literalStateMock.uiState['test_uiState.1']); - expect(instance.uiState['test_uiState.2']).not.toBe(literalStateMock.uiState['test_uiState.2']); - expect(instance.data['test_data.1']).not.toBe(literalStateMock.data['test_data.1']); - expect(instance.data['test_data.2']).not.toBe(literalStateMock.data['test_data.2']); - }); - }); + // When + const instance = stateMock.copy(partialState); - describe('|copy|', () => { - it('should verify will create copy from ComponentStateImpl', () => { - // When - const instance = stateMock.copy(); - - // Then - expect(instance).not.toBe(stateMock); - expect(instance).toEqual(stateMock); - }); - - it('should verify will merge provided State on top of original and return instance', () => { - // Given - const partialState: Partial = { - id: 'newId', - status: LOADED, - search: 'randomSearch', - routePath: 'domain/context/entity/20', - routePathSegments: ['domain/context', 'entity/20'], - data: new Map(), - uiState: new Map() - }; - - // When - const instance = stateMock.copy(partialState); - - // Then - expect(instance).not.toBe(stateMock); - expect(instance).not.toEqual(stateMock); - expect(instance.id).toEqual(partialState.id); - expect(instance.status).toEqual(partialState.status); - expect(instance.search).toEqual(partialState.search); - expect(instance.routePath).toEqual(partialState.routePath); - expect(instance.routePathSegments).toBe(partialState.routePathSegments); - expect(instance.data).toBe(partialState.data); - expect(instance.uiState).toBe(partialState.uiState); - }); - }); + // Then + expect(instance).not.toBe(stateMock); + expect(instance).not.toEqual(stateMock); + expect(instance.id).toEqual(partialState.id); + expect(instance.status).toEqual(partialState.status); + expect(instance.search).toEqual(partialState.search); + expect(instance.routePath).toEqual(partialState.routePath); + expect(instance.routePathSegments).toBe(partialState.routePathSegments); + expect(instance.data).toBe(partialState.data); + expect(instance.uiState).toBe(partialState.uiState); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts index f548024a6d..0b5394f961 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts @@ -5,432 +5,443 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; import { - Copy, - ErrorRecord, - ErrorStore, - Literal, - LiteralApiPredicates, - LiteralRequestPage, - RequestFilter, - RequestFilterImpl, - RequestOrder, - RequestOrderImpl, - RequestPage, - RequestPageImpl -} from '../../../../common'; - -import { ErrorStoreImpl } from '../../../error'; - -import { IDLE, StatusType } from './component-status.model'; + Copy, + ErrorRecord, + ErrorStore, + Literal, + LiteralApiPredicates, + LiteralRequestPage, + RequestFilter, + RequestFilterImpl, + RequestOrder, + RequestOrderImpl, + RequestPage, + RequestPageImpl, +} from "../../../../common"; + +import { ErrorStoreImpl } from "../../../error"; + +import { IDLE, StatusType } from "./component-status.model"; /** * ** Literal Component State in purest format ready for Store persisting. */ export interface LiteralComponentState { - /** - * ** Identifier for Component State. - */ - readonly id: string; - - /** - * ** Status for Component State. - */ - readonly status: StatusType; - - /** - * ** Component State Data. - *

    - * - Free format Literal Object. - *

    - */ - readonly data?: { [key: string]: any }; - - /** - * ** Route path for current State. - */ - readonly routePath?: string; - - /** - * ** Route path segments for current State. - */ - readonly routePathSegments?: string[]; - - /** - * ** Search query for Http requests. - */ - readonly search?: string; - - /** - * ** Page for Http requests. - */ - readonly page?: LiteralRequestPage; - - /** - * ** Order for Http requests. - */ - readonly order?: LiteralApiPredicates; - - /** - * ** Filter for Http requests. - */ - readonly filter?: LiteralApiPredicates; - - /** - * ** Order for Http requests. - */ - readonly requestParams?: { [key: string]: any }; - - /** - * ** Task is property that give bi-directional refinement context. - * - * - Gives context to Effect through Action. - * - Gives context to Component through ComponentState (ComponentModel). - */ - readonly task?: string; - - /** - * ** Router NavigationId bound to this Component State. - */ - readonly navigationId?: number; - - /** - * ** Error store for ErrorRecords that happen in stream manipulation down to the Components. - *

    - * - Ideal for storing Http errors and other runtime errors, so Component could easily leverage that knowledge and show info for User. - *

    - */ - readonly errors?: ErrorRecord[]; - - /** - * ** Component State UiState, that holds all information for UiElements. - *

    - * - Free format Literal Object where key identifier could be (Component/Html Element) name/id/class etc... - *

    - */ - readonly uiState?: { [key: string]: any }; + /** + * ** Identifier for Component State. + */ + readonly id: string; + + /** + * ** Status for Component State. + */ + readonly status: StatusType; + + /** + * ** Component State Data. + *

    + * - Free format Literal Object. + *

    + */ + readonly data?: { [key: string]: any }; + + /** + * ** Route path for current State. + */ + readonly routePath?: string; + + /** + * ** Route path segments for current State. + */ + readonly routePathSegments?: string[]; + + /** + * ** Search query for Http requests. + */ + readonly search?: string; + + /** + * ** Page for Http requests. + */ + readonly page?: LiteralRequestPage; + + /** + * ** Order for Http requests. + */ + readonly order?: LiteralApiPredicates; + + /** + * ** Filter for Http requests. + */ + readonly filter?: LiteralApiPredicates; + + /** + * ** Order for Http requests. + */ + readonly requestParams?: { [key: string]: any }; + + /** + * ** Task is property that give bi-directional refinement context. + * + * - Gives context to Effect through Action. + * - Gives context to Component through ComponentState (ComponentModel). + */ + readonly task?: string; + + /** + * ** Router NavigationId bound to this Component State. + */ + readonly navigationId?: number; + + /** + * ** Error store for ErrorRecords that happen in stream manipulation down to the Components. + *

    + * - Ideal for storing Http errors and other runtime errors, so Component could easily leverage that knowledge and show info for User. + *

    + */ + readonly errors?: ErrorRecord[]; + + /** + * ** Component State UiState, that holds all information for UiElements. + *

    + * - Free format Literal Object where key identifier could be (Component/Html Element) name/id/class etc... + *

    + */ + readonly uiState?: { [key: string]: any }; } -export interface ComponentState extends Literal, Copy { - /** - * ** Identifier for Component State. - */ - readonly id: string; - - /** - * ** Status for Component State. - */ - readonly status: StatusType; - - /** - * ** Component State Data. - *

    - * - Free format Map. - *

    - */ - readonly data?: Map; - - /** - * ** Route path for current State. - */ - readonly routePath?: string; - - /** - * ** Route Path Segments for current State. - */ - readonly routePathSegments?: string[]; - - /** - * ** Search query for Http requests. - */ - readonly search?: string; - - /** - * ** Page for Http requests. - */ - readonly page?: RequestPage; - - /** - * ** Order for Http requests. - */ - readonly order?: RequestOrder; - - /** - * ** Filter for Http requests. - */ - readonly filter?: RequestFilter; - - /** - * ** Map with different parameters for Http requests. - */ - readonly requestParams?: Map; - - /** - * ** Task is property that give bi-directional refinement context. - * - * - Gives context to Effect through Action. - * - Gives context to Component through ComponentState (ComponentModel). - */ - readonly task?: string; - - /** - * ** Router NavigationId bound to this Component State. - */ - readonly navigationId?: number; - - /** - * ** Error store for ErrorRecords that happen in stream manipulation down to the Components. - *

    - * - Ideal for storing Http errors and other runtime errors, so Component could easily leverage that knowledge and show info for User. - *

    - */ - readonly errors?: ErrorStore; - - /** - * ** Component State UiState, that holds all information for UiElements. - *

    - * - Free format Map where key identifier could be (Component/Html Element) name/id/class etc... - *

    - */ - readonly uiState?: Map; - - /** - * @inheritDoc - */ - toLiteral(): LiteralComponentState; - - /** - * @inheritDoc - */ - toLiteralCloneDeep(): LiteralComponentState; - - /** - * @inheritDoc - */ - copy(state?: Partial): ComponentState; +export interface ComponentState + extends Literal, Copy { + /** + * ** Identifier for Component State. + */ + readonly id: string; + + /** + * ** Status for Component State. + */ + readonly status: StatusType; + + /** + * ** Component State Data. + *

    + * - Free format Map. + *

    + */ + readonly data?: Map; + + /** + * ** Route path for current State. + */ + readonly routePath?: string; + + /** + * ** Route Path Segments for current State. + */ + readonly routePathSegments?: string[]; + + /** + * ** Search query for Http requests. + */ + readonly search?: string; + + /** + * ** Page for Http requests. + */ + readonly page?: RequestPage; + + /** + * ** Order for Http requests. + */ + readonly order?: RequestOrder; + + /** + * ** Filter for Http requests. + */ + readonly filter?: RequestFilter; + + /** + * ** Map with different parameters for Http requests. + */ + readonly requestParams?: Map; + + /** + * ** Task is property that give bi-directional refinement context. + * + * - Gives context to Effect through Action. + * - Gives context to Component through ComponentState (ComponentModel). + */ + readonly task?: string; + + /** + * ** Router NavigationId bound to this Component State. + */ + readonly navigationId?: number; + + /** + * ** Error store for ErrorRecords that happen in stream manipulation down to the Components. + *

    + * - Ideal for storing Http errors and other runtime errors, so Component could easily leverage that knowledge and show info for User. + *

    + */ + readonly errors?: ErrorStore; + + /** + * ** Component State UiState, that holds all information for UiElements. + *

    + * - Free format Map where key identifier could be (Component/Html Element) name/id/class etc... + *

    + */ + readonly uiState?: Map; + + /** + * @inheritDoc + */ + toLiteral(): LiteralComponentState; + + /** + * @inheritDoc + */ + toLiteralCloneDeep(): LiteralComponentState; + + /** + * @inheritDoc + */ + copy(state?: Partial): ComponentState; } /** * ** ComponentState implementation will all methods and other utilities. */ export class ComponentStateImpl implements ComponentState { - /** - * @inheritDoc - */ - readonly id: string; - - /** - * @inheritDoc - */ - readonly status: StatusType; - - /** - * @inheritDoc - */ - readonly data: Map; - - /** - * @inheritDoc - */ - readonly routePath: string; - - /** - * @inheritDoc - */ - readonly routePathSegments: string[]; - - /** - * @inheritDoc - */ - readonly search: string; - - /** - * @inheritDoc - */ - readonly page: RequestPageImpl; - - /** - * @inheritDoc - */ - readonly order: RequestOrderImpl; - - /** - * @inheritDoc - */ - readonly filter: RequestFilterImpl; - - /** - * @inheritDoc - */ - readonly requestParams: Map; - - /** - * @inheritDoc - */ - readonly task: string; - - /** - * @inheritDoc - */ - readonly navigationId: number; - - /** - * @inheritDoc - */ - readonly errors: ErrorStore; - - /** - * @inheritDoc - */ - readonly uiState: Map; - - /** - * ** Constructor. - * - *

    - * Important: - *

    - *

    - * If you add new Property in {@link LiteralComponentState}/{@link ComponentState} - *

      - *
    • - * Implement field in {@link ComponentStateImpl} and handle null/undefined, assign defaults (required for Collections). - *
    • - *
    • - * Copy/Clone process have to be handled manually (for performance gain) in methods: - * - * {@link ComponentStateImpl.fromLiteralComponentState} - * {@link ComponentStateImpl.cloneDeepLiteral} - * {@link ComponentStateImpl.toLiteral} - *
    • - *
    - *

    - */ - constructor(stateModelProp: Partial) { - const stateModel: Partial = CollectionsUtil.isDefined(stateModelProp) ? stateModelProp : {}; - - this.id = stateModel.id; - this.status = stateModel.status ?? IDLE; - this.navigationId = stateModel.navigationId ?? null; - this.routePath = stateModel.routePath; - this.routePathSegments = stateModel.routePathSegments ?? []; - this.search = stateModel.search ?? ''; - this.page = stateModel.page ?? RequestPageImpl.empty(); - this.order = stateModel.order ?? RequestOrderImpl.empty(); - this.filter = stateModel.filter ?? RequestFilterImpl.empty(); - this.requestParams = stateModel.requestParams ?? new Map(); - this.task = stateModel.task ?? null; - this.errors = stateModel.errors ?? ErrorStoreImpl.empty(); - this.data = stateModel.data ?? new Map(); - this.uiState = stateModel.uiState ?? new Map(); - } - - /** - * ** Factory method. - */ - static of(stateModel: Partial): ComponentStateImpl { - return new ComponentStateImpl(stateModel); - } - - /** - * ** Convert provided {@link LiteralComponentState} into instance of {@link ComponentStateImpl}. - *

    - * Every literals could be transformed to their original Collection format. - *

      - *
    • - * Object literals could be transformed to Map/WeakMap/Set depends of the needs. - *
    • - *
    • - * Array is keep as it is. - *
    • - *
    - *

    - * - * @see CollectionsUtil.transformObjectToMap - * @see CollectionsUtil.transformMapToObject - */ - static fromLiteralComponentState(literalStateModel: LiteralComponentState): ComponentStateImpl { - return ComponentStateImpl.of({ - ...literalStateModel, - errors: ErrorStoreImpl.fromLiteral(literalStateModel.errors), - page: RequestPageImpl.fromLiteral(literalStateModel.page), - order: RequestOrderImpl.fromLiteral(literalStateModel.order), - filter: RequestFilterImpl.fromLiteral(literalStateModel.filter), - requestParams: CollectionsUtil.transformObjectToMap(literalStateModel.requestParams), - data: CollectionsUtil.transformObjectToMap(literalStateModel.data), - uiState: CollectionsUtil.transformObjectToMap(literalStateModel.uiState) - }); - } - - /** - * ** Make deep clone from Literal Component State. - */ - static cloneDeepLiteral(literalStateModel: LiteralComponentState): LiteralComponentState { - return { - id: literalStateModel.id, - status: literalStateModel.status, - data: CollectionsUtil.cloneDeep(literalStateModel.data), - routePath: literalStateModel.routePath, - routePathSegments: [...literalStateModel.routePathSegments], - search: literalStateModel.search, - page: CollectionsUtil.cloneDeep(literalStateModel.page), - order: CollectionsUtil.cloneDeep(literalStateModel.order), - filter: CollectionsUtil.cloneDeep(literalStateModel.filter), - requestParams: CollectionsUtil.cloneDeep(literalStateModel.requestParams), - task: literalStateModel.task, - navigationId: literalStateModel.navigationId, - errors: ErrorStoreImpl.cloneDeepErrorRecords(literalStateModel.errors), - uiState: CollectionsUtil.cloneDeep(literalStateModel.uiState) - }; - } - - /** - *

    - * Every Collection should be transformed to format of JSON supported literals, ready for LocalStorage/SessionStorage persist. - *

      - *
    • - * Map/WeakMap/Set have to be transform to Object literal. - *
    • - *
    • - * Array is keep as it is. - *
    • - *
    - *

    - * - * @see CollectionsUtil.transformObjectToMap - * @see CollectionsUtil.transformMapToObject - * - * @inheritDoc - */ - toLiteral(): LiteralComponentState { - return { - ...this, - page: this.page.toLiteral(), - order: this.order.toLiteral(), - filter: this.filter.toLiteral(), - errors: this.errors.toLiteral(), - requestParams: CollectionsUtil.transformMapToObject(this.requestParams), - data: CollectionsUtil.transformMapToObject(this.data), - uiState: CollectionsUtil.transformMapToObject(this.uiState) - }; - } - - /** - * @inheritDoc - */ - toLiteralCloneDeep(): LiteralComponentState { - return ComponentStateImpl.cloneDeepLiteral(this.toLiteral()); - } - - /** - * @inheritDoc - */ - copy(state: Partial = {}): ComponentStateImpl { - return ComponentStateImpl.of({ - ...this, - ...state - }); - } + /** + * @inheritDoc + */ + readonly id: string; + + /** + * @inheritDoc + */ + readonly status: StatusType; + + /** + * @inheritDoc + */ + readonly data: Map; + + /** + * @inheritDoc + */ + readonly routePath: string; + + /** + * @inheritDoc + */ + readonly routePathSegments: string[]; + + /** + * @inheritDoc + */ + readonly search: string; + + /** + * @inheritDoc + */ + readonly page: RequestPageImpl; + + /** + * @inheritDoc + */ + readonly order: RequestOrderImpl; + + /** + * @inheritDoc + */ + readonly filter: RequestFilterImpl; + + /** + * @inheritDoc + */ + readonly requestParams: Map; + + /** + * @inheritDoc + */ + readonly task: string; + + /** + * @inheritDoc + */ + readonly navigationId: number; + + /** + * @inheritDoc + */ + readonly errors: ErrorStore; + + /** + * @inheritDoc + */ + readonly uiState: Map; + + /** + * ** Constructor. + * + *

    + * Important: + *

    + *

    + * If you add new Property in {@link LiteralComponentState}/{@link ComponentState} + *

      + *
    • + * Implement field in {@link ComponentStateImpl} and handle null/undefined, assign defaults (required for Collections). + *
    • + *
    • + * Copy/Clone process have to be handled manually (for performance gain) in methods: + * + * {@link ComponentStateImpl.fromLiteralComponentState} + * {@link ComponentStateImpl.cloneDeepLiteral} + * {@link ComponentStateImpl.toLiteral} + *
    • + *
    + *

    + */ + constructor(stateModelProp: Partial) { + const stateModel: Partial = CollectionsUtil.isDefined( + stateModelProp, + ) + ? stateModelProp + : {}; + + this.id = stateModel.id; + this.status = stateModel.status ?? IDLE; + this.navigationId = stateModel.navigationId ?? null; + this.routePath = stateModel.routePath; + this.routePathSegments = stateModel.routePathSegments ?? []; + this.search = stateModel.search ?? ""; + this.page = stateModel.page ?? RequestPageImpl.empty(); + this.order = stateModel.order ?? RequestOrderImpl.empty(); + this.filter = stateModel.filter ?? RequestFilterImpl.empty(); + this.requestParams = stateModel.requestParams ?? new Map(); + this.task = stateModel.task ?? null; + this.errors = stateModel.errors ?? ErrorStoreImpl.empty(); + this.data = stateModel.data ?? new Map(); + this.uiState = stateModel.uiState ?? new Map(); + } + + /** + * ** Factory method. + */ + static of(stateModel: Partial): ComponentStateImpl { + return new ComponentStateImpl(stateModel); + } + + /** + * ** Convert provided {@link LiteralComponentState} into instance of {@link ComponentStateImpl}. + *

    + * Every literals could be transformed to their original Collection format. + *

      + *
    • + * Object literals could be transformed to Map/WeakMap/Set depends of the needs. + *
    • + *
    • + * Array is keep as it is. + *
    • + *
    + *

    + * + * @see CollectionsUtil.transformObjectToMap + * @see CollectionsUtil.transformMapToObject + */ + static fromLiteralComponentState( + literalStateModel: LiteralComponentState, + ): ComponentStateImpl { + return ComponentStateImpl.of({ + ...literalStateModel, + errors: ErrorStoreImpl.fromLiteral(literalStateModel.errors), + page: RequestPageImpl.fromLiteral(literalStateModel.page), + order: RequestOrderImpl.fromLiteral(literalStateModel.order), + filter: RequestFilterImpl.fromLiteral(literalStateModel.filter), + requestParams: CollectionsUtil.transformObjectToMap( + literalStateModel.requestParams, + ), + data: CollectionsUtil.transformObjectToMap(literalStateModel.data), + uiState: CollectionsUtil.transformObjectToMap(literalStateModel.uiState), + }); + } + + /** + * ** Make deep clone from Literal Component State. + */ + static cloneDeepLiteral( + literalStateModel: LiteralComponentState, + ): LiteralComponentState { + return { + id: literalStateModel.id, + status: literalStateModel.status, + data: CollectionsUtil.cloneDeep(literalStateModel.data), + routePath: literalStateModel.routePath, + routePathSegments: [...literalStateModel.routePathSegments], + search: literalStateModel.search, + page: CollectionsUtil.cloneDeep(literalStateModel.page), + order: CollectionsUtil.cloneDeep(literalStateModel.order), + filter: CollectionsUtil.cloneDeep(literalStateModel.filter), + requestParams: CollectionsUtil.cloneDeep(literalStateModel.requestParams), + task: literalStateModel.task, + navigationId: literalStateModel.navigationId, + errors: ErrorStoreImpl.cloneDeepErrorRecords(literalStateModel.errors), + uiState: CollectionsUtil.cloneDeep(literalStateModel.uiState), + }; + } + + /** + *

    + * Every Collection should be transformed to format of JSON supported literals, ready for LocalStorage/SessionStorage persist. + *

      + *
    • + * Map/WeakMap/Set have to be transform to Object literal. + *
    • + *
    • + * Array is keep as it is. + *
    • + *
    + *

    + * + * @see CollectionsUtil.transformObjectToMap + * @see CollectionsUtil.transformMapToObject + * + * @inheritDoc + */ + toLiteral(): LiteralComponentState { + return { + ...this, + page: this.page.toLiteral(), + order: this.order.toLiteral(), + filter: this.filter.toLiteral(), + errors: this.errors.toLiteral(), + requestParams: CollectionsUtil.transformMapToObject(this.requestParams), + data: CollectionsUtil.transformMapToObject(this.data), + uiState: CollectionsUtil.transformMapToObject(this.uiState), + }; + } + + /** + * @inheritDoc + */ + toLiteralCloneDeep(): LiteralComponentState { + return ComponentStateImpl.cloneDeepLiteral(this.toLiteral()); + } + + /** + * @inheritDoc + */ + copy(state: Partial = {}): ComponentStateImpl { + return ComponentStateImpl.of({ + ...this, + ...state, + }); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts index 7673753a01..cfc3d56b33 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts @@ -8,29 +8,34 @@ /** * ** Status constant for Initialized. */ -export const INITIALIZED = 'Initialized'; +export const INITIALIZED = "Initialized"; /** * ** Status constant for Idle. */ -export const IDLE = 'Idle'; +export const IDLE = "Idle"; /** * ** Status constant for Loading. */ -export const LOADING = 'Loading'; +export const LOADING = "Loading"; /** * ** Status constant for Loaded. */ -export const LOADED = 'Loaded'; +export const LOADED = "Loaded"; /** * ** Status constant for Failed. */ -export const FAILED = 'Failed'; +export const FAILED = "Failed"; /** * ** Status types. */ -export type StatusType = typeof INITIALIZED | typeof IDLE | typeof LOADING | typeof LOADED | typeof FAILED; +export type StatusType = + | typeof INITIALIZED + | typeof IDLE + | typeof LOADING + | typeof LOADED + | typeof FAILED; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts index 9df2c5c89c..c771587eb1 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts @@ -5,324 +5,392 @@ /* eslint-disable @typescript-eslint/dot-notation,arrow-body-style,prefer-arrow/prefer-arrow-functions */ -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; + +import { FAILED, IDLE, LOADED, LOADING } from "./component-status.model"; + +import { + ComponentState, + ComponentStateImpl, + LiteralComponentState, +} from "./component-state.model"; + +import { + ComponentsStateHelper, + LiteralComponentsState, +} from "./components-state.model"; + +describe("ComponentsStateHelper", () => { + it("should verify instance is created", () => { + // When + const instance = new ComponentsStateHelper(); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Methods::", () => { + let literalComponentsState: LiteralComponentsState; + let helper: ComponentsStateHelper; + let componentState1: ComponentState; + let componentState2: ComponentState; + let componentState3: ComponentState; + let componentState4: ComponentState; + + beforeEach(() => { + componentState1 = ComponentStateImpl.of({ + id: "component1", + status: LOADED, + }); + componentState2 = ComponentStateImpl.of({ + id: "component2", + status: IDLE, + }); + componentState3 = ComponentStateImpl.of({ + id: "component3", + status: FAILED, + }); + componentState4 = ComponentStateImpl.of({ + id: "component4", + status: IDLE, + }); + + literalComponentsState = createComponentsLiteralState( + componentState1, + componentState2, + componentState3, + componentState4, + ); + + helper = new ComponentsStateHelper(); + helper.setState(literalComponentsState); + }); + + describe("|getState|", () => { + it("should verify will return literalComponentState", () => { + // When + const state = helper.getState(); + + // Then + expect(state).toBeDefined(); + expect(state).toEqual(literalComponentsState); + expect(state.components).not.toBe(literalComponentsState.components); + expect(state.routePathSegments).not.toBe( + literalComponentsState.routePathSegments, + ); + }); + }); -import { FAILED, IDLE, LOADED, LOADING } from './component-status.model'; + describe("|getLiteralComponentState|", () => { + it("should verify will return LiteralComponentState", () => { + // When + const state = helper.getLiteralComponentState("component3", [ + "test_domain/context", + "test_entity/10", + ]); -import { ComponentState, ComponentStateImpl, LiteralComponentState } from './component-state.model'; + // Then + expect(state).toBeDefined(); + expect(state).toEqual( + literalComponentsState.routePathSegments["test_domain/context"] + .routePathSegments["test_entity/10"].components["component3"], + ); + }); + + it("should verify will return null if there is no such state v1", () => { + // When + const state = helper.getLiteralComponentState("component10", [ + "test_domain/context", + "test_entity/10", + ]); -import { ComponentsStateHelper, LiteralComponentsState } from './components-state.model'; + // Then + expect(state).toEqual(null); + }); -describe('ComponentsStateHelper', () => { - it('should verify instance is created', () => { + it("should verify will return null if there is no such state v2", () => { // When - const instance = new ComponentsStateHelper(); + const state = helper.getLiteralComponentState("component10"); // Then - expect(instance).toBeDefined(); + expect(state).toEqual(null); + }); + + it("should verify will return null if there is no such routePathSegments", () => { + // When + const state = helper.getLiteralComponentState("component3", [ + "test_domain/context", + "entity/15", + ]); + + // Then + expect(state).toEqual(null); + }); }); - describe('Methods::', () => { - let literalComponentsState: LiteralComponentsState; - let helper: ComponentsStateHelper; - let componentState1: ComponentState; - let componentState2: ComponentState; - let componentState3: ComponentState; - let componentState4: ComponentState; + describe("|getComponentState|", () => { + it("should verify will return ComponentState", () => { + // When + const state = helper.getComponentState("component2", [ + "test_domain/context", + "test_entity/18", + ]); - beforeEach(() => { - componentState1 = ComponentStateImpl.of({ id: 'component1', status: LOADED }); - componentState2 = ComponentStateImpl.of({ id: 'component2', status: IDLE }); - componentState3 = ComponentStateImpl.of({ id: 'component3', status: FAILED }); - componentState4 = ComponentStateImpl.of({ id: 'component4', status: IDLE }); + // Then + expect(state).toBeDefined(); + expect(state).toEqual( + componentState2.copy({ + routePath: "domain/context/entity/18", + routePathSegments: ["test_domain/context", "test_entity/18"], + }), + ); + expect(state.routePathSegments).not.toBe( + componentState2.routePathSegments, + ); + expect(state.data).not.toBe(componentState2.data); + expect(state.uiState).not.toBe(componentState2.uiState); + }); + + it("should verify will return null if there is no such state v1", () => { + // When + const state = helper.getComponentState("component4", [ + "test_domain/context", + ]); - literalComponentsState = createComponentsLiteralState(componentState1, componentState2, componentState3, componentState4); + // Then + expect(state).toEqual(null); + }); - helper = new ComponentsStateHelper(); - helper.setState(literalComponentsState); - }); + it("should verify will return null if there is no such state v2", () => { + // When + const state = helper.getComponentState("component4"); - describe('|getState|', () => { - it('should verify will return literalComponentState', () => { - // When - const state = helper.getState(); - - // Then - expect(state).toBeDefined(); - expect(state).toEqual(literalComponentsState); - expect(state.components).not.toBe(literalComponentsState.components); - expect(state.routePathSegments).not.toBe(literalComponentsState.routePathSegments); - }); - }); + // Then + expect(state).toEqual(null); + }); - describe('|getLiteralComponentState|', () => { - it('should verify will return LiteralComponentState', () => { - // When - const state = helper.getLiteralComponentState('component3', ['test_domain/context', 'test_entity/10']); - - // Then - expect(state).toBeDefined(); - expect(state).toEqual( - literalComponentsState.routePathSegments['test_domain/context'].routePathSegments['test_entity/10'].components[ - 'component3' - ] - ); - }); - - it('should verify will return null if there is no such state v1', () => { - // When - const state = helper.getLiteralComponentState('component10', ['test_domain/context', 'test_entity/10']); - - // Then - expect(state).toEqual(null); - }); - - it('should verify will return null if there is no such state v2', () => { - // When - const state = helper.getLiteralComponentState('component10'); - - // Then - expect(state).toEqual(null); - }); - - it('should verify will return null if there is no such routePathSegments', () => { - // When - const state = helper.getLiteralComponentState('component3', ['test_domain/context', 'entity/15']); - - // Then - expect(state).toEqual(null); - }); - }); + it("should verify will return null if there is no such routePathSegments", () => { + // When + const state = helper.getLiteralComponentState("component2", [ + "domain/explore", + "test_entity/10", + ]); - describe('|getComponentState|', () => { - it('should verify will return ComponentState', () => { - // When - const state = helper.getComponentState('component2', ['test_domain/context', 'test_entity/18']); - - // Then - expect(state).toBeDefined(); - expect(state).toEqual( - componentState2.copy({ - routePath: 'domain/context/entity/18', - routePathSegments: ['test_domain/context', 'test_entity/18'] - }) - ); - expect(state.routePathSegments).not.toBe(componentState2.routePathSegments); - expect(state.data).not.toBe(componentState2.data); - expect(state.uiState).not.toBe(componentState2.uiState); - }); - - it('should verify will return null if there is no such state v1', () => { - // When - const state = helper.getComponentState('component4', ['test_domain/context']); - - // Then - expect(state).toEqual(null); - }); - - it('should verify will return null if there is no such state v2', () => { - // When - const state = helper.getComponentState('component4'); - - // Then - expect(state).toEqual(null); - }); - - it('should verify will return null if there is no such routePathSegments', () => { - // When - const state = helper.getLiteralComponentState('component2', ['domain/explore', 'test_entity/10']); - - // Then - expect(state).toEqual(null); - }); - }); + // Then + expect(state).toEqual(null); + }); + }); - describe('|getAllComponentState|', () => { - it('should verify will return Array of all ComponentState in given routePathSegments', () => { - // When - const states = helper.getAllComponentState(['test_domain/context', 'test_entity/10']); - - // Then - expect(states.length).toEqual(8); - expect(states).toEqual([ - componentState1, - componentState3, - componentState2.copy({ - routePath: 'test_domain/context', - routePathSegments: ['test_domain/context'] - }), - componentState3.copy({ - routePath: 'test_domain/context', - routePathSegments: ['test_domain/context'] - }), - componentState2.copy({ - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }), - componentState4.copy({ - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }), - componentState1.copy({ - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }), - componentState3.copy({ - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }) - ]); - }); - - it('should verify will return Array of all ComponentState in given base routePathSegments', () => { - // When - const states = helper.getAllComponentState(null); - - // Then - expect(states.length).toEqual(2); - expect(states).toEqual([componentState1, componentState3]); - }); - }); + describe("|getAllComponentState|", () => { + it("should verify will return Array of all ComponentState in given routePathSegments", () => { + // When + const states = helper.getAllComponentState([ + "test_domain/context", + "test_entity/10", + ]); - describe('|updateLiteralComponentState|', () => { - it('should verify will update state in right routePathSegments', () => { - // Given - const literalState: LiteralComponentState = { - ...componentState1.toLiteral(), - status: LOADING, - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'], - data: { payload: { content: { data: [1, 2, 3] } } }, - uiState: { element1: { click: true } } - }; - - // Then 1 - expect( - helper.getState().routePathSegments['test_domain/context'].routePathSegments['test_entity/10'].components['component1'] - ).not.toEqual(literalState); - - // When - helper.updateLiteralComponentState(literalState); - - // Then 2 - expect( - helper.getState().routePathSegments['test_domain/context'].routePathSegments['test_entity/10'].components['component1'] - ).toEqual(literalState); - }); - }); + // Then + expect(states.length).toEqual(8); + expect(states).toEqual([ + componentState1, + componentState3, + componentState2.copy({ + routePath: "test_domain/context", + routePathSegments: ["test_domain/context"], + }), + componentState3.copy({ + routePath: "test_domain/context", + routePathSegments: ["test_domain/context"], + }), + componentState2.copy({ + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }), + componentState4.copy({ + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }), + componentState1.copy({ + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }), + componentState3.copy({ + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }), + ]); + }); + + it("should verify will return Array of all ComponentState in given base routePathSegments", () => { + // When + const states = helper.getAllComponentState(null); - describe('|resetComponentStates|', () => { - it('should verify will reset all ComponentStata status in given routePathSegments', () => { - // When - helper.resetComponentStates(['test_domain/context', 'test_entity/10']); - const state = helper.getState(); - - // Then - CollectionsUtil.iterateObject(state.components, (value) => { - expect(value.status).toEqual(IDLE); - }); - - CollectionsUtil.iterateObject(state.routePathSegments['test_domain/context'].components, (value) => { - expect(value.status).toEqual(IDLE); - }); - - CollectionsUtil.iterateObject( - state.routePathSegments['test_domain/context'].routePathSegments['test_entity/10'].components, - (value) => { - expect(value.status).toEqual(IDLE); - } - ); - }); - }); + // Then + expect(states.length).toEqual(2); + expect(states).toEqual([componentState1, componentState3]); + }); + }); - describe('|deleteRoutePathSegments|', () => { - it('should verify will delete routePathSegment', () => { - // Then 1 - expect(helper.getState().routePathSegments['test_domain/context'].routePathSegments['test_entity/18']).toBeDefined(); + describe("|updateLiteralComponentState|", () => { + it("should verify will update state in right routePathSegments", () => { + // Given + const literalState: LiteralComponentState = { + ...componentState1.toLiteral(), + status: LOADING, + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + data: { payload: { content: { data: [1, 2, 3] } } }, + uiState: { element1: { click: true } }, + }; + + // Then 1 + expect( + helper.getState().routePathSegments["test_domain/context"] + .routePathSegments["test_entity/10"].components["component1"], + ).not.toEqual(literalState); - // When - helper.deleteRoutePathSegments(['test_domain/context', 'test_entity/18']); + // When + helper.updateLiteralComponentState(literalState); + + // Then 2 + expect( + helper.getState().routePathSegments["test_domain/context"] + .routePathSegments["test_entity/10"].components["component1"], + ).toEqual(literalState); + }); + }); + + describe("|resetComponentStates|", () => { + it("should verify will reset all ComponentStata status in given routePathSegments", () => { + // When + helper.resetComponentStates(["test_domain/context", "test_entity/10"]); + const state = helper.getState(); - // Then 2 - expect(helper.getState().routePathSegments['test_domain/context'].routePathSegments['test_entity/18']).toBeUndefined(); - }); + // Then + CollectionsUtil.iterateObject(state.components, (value) => { + expect(value.status).toEqual(IDLE); }); + + CollectionsUtil.iterateObject( + state.routePathSegments["test_domain/context"].components, + (value) => { + expect(value.status).toEqual(IDLE); + }, + ); + + CollectionsUtil.iterateObject( + state.routePathSegments["test_domain/context"].routePathSegments[ + "test_entity/10" + ].components, + (value) => { + expect(value.status).toEqual(IDLE); + }, + ); + }); }); + + describe("|deleteRoutePathSegments|", () => { + it("should verify will delete routePathSegment", () => { + // Then 1 + expect( + helper.getState().routePathSegments["test_domain/context"] + .routePathSegments["test_entity/18"], + ).toBeDefined(); + + // When + helper.deleteRoutePathSegments([ + "test_domain/context", + "test_entity/18", + ]); + + // Then 2 + expect( + helper.getState().routePathSegments["test_domain/context"] + .routePathSegments["test_entity/18"], + ).toBeUndefined(); + }); + }); + }); }); -const createComponentsLiteralState = (c1: ComponentState, c2: ComponentState, c3: ComponentState, c4: ComponentState) => { - return { +const createComponentsLiteralState = ( + c1: ComponentState, + c2: ComponentState, + c3: ComponentState, + c4: ComponentState, +) => { + return { + components: { + component1: { ...c1.toLiteral() }, + component3: { ...c3.toLiteral() }, + }, + routePathSegments: { + "test_domain/context": { components: { - component1: { ...c1.toLiteral() }, - component3: { ...c3.toLiteral() } + component2: { + ...c2.toLiteral(), + routePath: "test_domain/context", + routePathSegments: ["test_domain/context"], + }, + component3: { + ...c3.toLiteral(), + routePath: "test_domain/context", + routePathSegments: ["test_domain/context"], + }, }, routePathSegments: { - 'test_domain/context': { - components: { - component2: { - ...c2.toLiteral(), - routePath: 'test_domain/context', - routePathSegments: ['test_domain/context'] - }, - component3: { - ...c3.toLiteral(), - routePath: 'test_domain/context', - routePathSegments: ['test_domain/context'] - } - }, - routePathSegments: { - 'test_entity/10': { - components: { - component2: { - ...c2.toLiteral(), - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }, - component4: { - ...c4.toLiteral(), - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }, - component1: { - ...c1.toLiteral(), - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - }, - component3: { - ...c3.toLiteral(), - routePath: 'domain/context/entity/10', - routePathSegments: ['test_domain/context', 'test_entity/10'] - } - }, - routePathSegments: {} - }, - 'test_entity/18': { - components: { - component3: { - ...c3.toLiteral(), - routePath: 'domain/context/entity/18', - routePathSegments: ['test_domain/context', 'test_entity/18'] - }, - component2: { - ...c2.toLiteral(), - routePath: 'domain/context/entity/18', - routePathSegments: ['test_domain/context', 'test_entity/18'] - }, - component1: { - ...c1.toLiteral(), - routePath: 'domain/context/entity/18', - routePathSegments: ['test_domain/context', 'test_entity/18'] - }, - component4: { - ...c4.toLiteral(), - routePath: 'domain/context/entity/18', - routePathSegments: ['test_domain/context', 'test_entity/18'] - } - }, - routePathSegments: {} - } - } - } - } - } as LiteralComponentsState; + "test_entity/10": { + components: { + component2: { + ...c2.toLiteral(), + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }, + component4: { + ...c4.toLiteral(), + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }, + component1: { + ...c1.toLiteral(), + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }, + component3: { + ...c3.toLiteral(), + routePath: "domain/context/entity/10", + routePathSegments: ["test_domain/context", "test_entity/10"], + }, + }, + routePathSegments: {}, + }, + "test_entity/18": { + components: { + component3: { + ...c3.toLiteral(), + routePath: "domain/context/entity/18", + routePathSegments: ["test_domain/context", "test_entity/18"], + }, + component2: { + ...c2.toLiteral(), + routePath: "domain/context/entity/18", + routePathSegments: ["test_domain/context", "test_entity/18"], + }, + component1: { + ...c1.toLiteral(), + routePath: "domain/context/entity/18", + routePathSegments: ["test_domain/context", "test_entity/18"], + }, + component4: { + ...c4.toLiteral(), + routePath: "domain/context/entity/18", + routePathSegments: ["test_domain/context", "test_entity/18"], + }, + }, + routePathSegments: {}, + }, + }, + }, + }, + } as LiteralComponentsState; }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts index 6c6971be2c..a70d63c096 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts @@ -3,250 +3,311 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { IDLE } from './component-status.model'; -import { ComponentState, ComponentStateImpl, LiteralComponentState } from './component-state.model'; +import { IDLE } from "./component-status.model"; +import { + ComponentState, + ComponentStateImpl, + LiteralComponentState, +} from "./component-state.model"; export interface LiteralComponentsState { - readonly components: { [name: string]: LiteralComponentState }; - readonly routePathSegments: { [segmentId: string]: LiteralComponentsState }; + readonly components: { [name: string]: LiteralComponentState }; + readonly routePathSegments: { [segmentId: string]: LiteralComponentsState }; } /** * ** ComponentsState Helper. */ export class ComponentsStateHelper { - private _literalComponentsState: LiteralComponentsState; - - constructor() { - this._literalComponentsState = { - components: {}, - routePathSegments: {} - }; - } - - /** - * ** Returns LiteralComponentsState from Helper. - */ - getState(): LiteralComponentsState { - return { - ...this._literalComponentsState - }; + private _literalComponentsState: LiteralComponentsState; + + constructor() { + this._literalComponentsState = { + components: {}, + routePathSegments: {}, + }; + } + + /** + * ** Returns LiteralComponentsState from Helper. + */ + getState(): LiteralComponentsState { + return { + ...this._literalComponentsState, + }; + } + + /** + * ** Will set state to the local Helper state. + */ + setState(literalComponentsState: LiteralComponentsState) { + this._literalComponentsState = this._shallowCloneComponentsState( + literalComponentsState, + ); + + return this; + } + + /** + * ** Will return LiteralComponentState for given id and routePathSegments. + */ + getLiteralComponentState( + id: string, + routePathSegments?: string[], + ): LiteralComponentState { + return this._getLiteralComponentState( + id, + CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], + this._literalComponentsState, + ); + } + + /** + * ** Get ComponentState for given id and routePathSegments. + */ + getComponentState(id: string, routePathSegments?: string[]): ComponentState { + const literalComponentState = this._getLiteralComponentState( + id, + CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], + this._literalComponentsState, + ); + + return CollectionsUtil.isDefined(literalComponentState) + ? ComponentStateImpl.fromLiteralComponentState(literalComponentState) + : null; + } + + /** + * ** Get all ComponentState for given routePathSegments. + */ + getAllComponentState(routePathSegments: string[]): ComponentState[] { + return this._getAllComponentState( + CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], + this._literalComponentsState, + ); + } + + /** + * ** Update LiteralComponentState. + */ + updateLiteralComponentState( + literalComponentState: LiteralComponentState, + ): void { + return this._updateLiteralComponentState( + literalComponentState, + [...literalComponentState.routePathSegments], + this._literalComponentsState, + ); + } + + /** + * ** Reset component status to NOT_LOADED for all ComponentState in a given routePathSegment. + */ + resetComponentStates(routePathSegments: string[]): void { + this._resetComponentStates( + CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], + this._literalComponentsState, + ); + } + + /** + * ** Delete all ComponentState for given routePathSegment. + */ + deleteRoutePathSegments(routePathSegments: string[]): void { + this._deleteRoutePathSegments( + CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], + this._literalComponentsState, + ); + } + + /** + * ** Update ComponentState. + */ + private _updateLiteralComponentState( + literalComponentState: LiteralComponentState, + routePathSegments: string[], + state: LiteralComponentsState, + ): void { + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + state.components[literalComponentState.id] = literalComponentState; + + return; } - /** - * ** Will set state to the local Helper state. - */ - setState(literalComponentsState: LiteralComponentsState) { - this._literalComponentsState = this._shallowCloneComponentsState(literalComponentsState); - - return this; + const routePathSegment = routePathSegments.shift(); + + this._updateLiteralComponentState( + literalComponentState, + routePathSegments, + this._normalizeRoutePathSegments( + state.routePathSegments, + routePathSegment, + ), + ); + } + + /** + * ** Get ComponentState. + */ + private _getLiteralComponentState( + id: string, + routePathSegments: string[], + state: LiteralComponentsState, + ): LiteralComponentState | null { + if (!state) { + return null; } - /** - * ** Will return LiteralComponentState for given id and routePathSegments. - */ - getLiteralComponentState(id: string, routePathSegments?: string[]): LiteralComponentState { - return this._getLiteralComponentState( - id, - CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], - this._literalComponentsState - ); - } - - /** - * ** Get ComponentState for given id and routePathSegments. - */ - getComponentState(id: string, routePathSegments?: string[]): ComponentState { - const literalComponentState = this._getLiteralComponentState( - id, - CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], - this._literalComponentsState - ); - - return CollectionsUtil.isDefined(literalComponentState) - ? ComponentStateImpl.fromLiteralComponentState(literalComponentState) - : null; - } + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + if (state.components[id]) { + return ComponentStateImpl.cloneDeepLiteral(state.components[id]); + } - /** - * ** Get all ComponentState for given routePathSegments. - */ - getAllComponentState(routePathSegments: string[]): ComponentState[] { - return this._getAllComponentState( - CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], - this._literalComponentsState - ); + return null; } - /** - * ** Update LiteralComponentState. - */ - updateLiteralComponentState(literalComponentState: LiteralComponentState): void { - return this._updateLiteralComponentState( - literalComponentState, - [...literalComponentState.routePathSegments], - this._literalComponentsState - ); + const routePathSegment = routePathSegments.shift(); + + return this._getLiteralComponentState( + id, + routePathSegments, + state.routePathSegments[routePathSegment], + ); + } + + /** + * ** Get all components for given routePathSegments. + */ + private _getAllComponentState( + routePathSegments: string[], + state: LiteralComponentsState, + ): ComponentState[] { + if (!state) { + return []; } - /** - * ** Reset component status to NOT_LOADED for all ComponentState in a given routePathSegment. - */ - resetComponentStates(routePathSegments: string[]): void { - this._resetComponentStates(CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], this._literalComponentsState); - } + const components: ComponentState[] = CollectionsUtil.objectValues( + state.components, + ).map((c) => + ComponentStateImpl.fromLiteralComponentState( + ComponentStateImpl.cloneDeepLiteral(c), + ), + ); - /** - * ** Delete all ComponentState for given routePathSegment. - */ - deleteRoutePathSegments(routePathSegments: string[]): void { - this._deleteRoutePathSegments( - CollectionsUtil.isArray(routePathSegments) ? [...routePathSegments] : [], - this._literalComponentsState - ); + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + return components; } - /** - * ** Update ComponentState. - */ - private _updateLiteralComponentState( - literalComponentState: LiteralComponentState, - routePathSegments: string[], - state: LiteralComponentsState - ): void { - if (CollectionsUtil.isArrayEmpty(routePathSegments)) { - state.components[literalComponentState.id] = literalComponentState; - - return; - } - - const routePathSegment = routePathSegments.shift(); - - this._updateLiteralComponentState( - literalComponentState, - routePathSegments, - this._normalizeRoutePathSegments(state.routePathSegments, routePathSegment) - ); + const routePathSegment = routePathSegments.shift(); + + return [ + ...components, + ...this._getAllComponentState( + routePathSegments, + state.routePathSegments[routePathSegment], + ), + ]; + } + + /** + * ** Reset component status to NOT_LOADED for all component in a given context. + */ + private _resetComponentStates( + routePathSegments: string[], + state: LiteralComponentsState, + ): void { + CollectionsUtil.iterateObject(state.components, (componentState, id) => { + state.components[id] = { ...componentState, status: IDLE }; + }); + + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + return; } - /** - * ** Get ComponentState. - */ - private _getLiteralComponentState( - id: string, - routePathSegments: string[], - state: LiteralComponentsState - ): LiteralComponentState | null { - if (!state) { - return null; - } - - if (CollectionsUtil.isArrayEmpty(routePathSegments)) { - if (state.components[id]) { - return ComponentStateImpl.cloneDeepLiteral(state.components[id]); - } - - return null; - } - - const routePathSegment = routePathSegments.shift(); - - return this._getLiteralComponentState(id, routePathSegments, state.routePathSegments[routePathSegment]); + const routePathSegment = routePathSegments.shift(); + + this._resetComponentStates( + routePathSegments, + this._normalizeRoutePathSegments( + state.routePathSegments, + routePathSegment, + ), + ); + } + + /** + * ** Delete all components state for a given route path segment. + */ + private _deleteRoutePathSegments( + routePathSegments: string[], + state: LiteralComponentsState, + ): void { + const routePathSegment = routePathSegments.shift(); + + if (!routePathSegment) { + return; } - /** - * ** Get all components for given routePathSegments. - */ - private _getAllComponentState(routePathSegments: string[], state: LiteralComponentsState): ComponentState[] { - if (!state) { - return []; - } - - const components: ComponentState[] = CollectionsUtil.objectValues(state.components).map((c) => - ComponentStateImpl.fromLiteralComponentState(ComponentStateImpl.cloneDeepLiteral(c)) - ); - - if (CollectionsUtil.isArrayEmpty(routePathSegments)) { - return components; - } - - const routePathSegment = routePathSegments.shift(); + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + delete state.routePathSegments[routePathSegment]; - return [...components, ...this._getAllComponentState(routePathSegments, state.routePathSegments[routePathSegment])]; + return; } - /** - * ** Reset component status to NOT_LOADED for all component in a given context. - */ - private _resetComponentStates(routePathSegments: string[], state: LiteralComponentsState): void { - CollectionsUtil.iterateObject(state.components, (componentState, id) => { - state.components[id] = { ...componentState, status: IDLE }; - }); - - if (CollectionsUtil.isArrayEmpty(routePathSegments)) { - return; - } - - const routePathSegment = routePathSegments.shift(); - - this._resetComponentStates(routePathSegments, this._normalizeRoutePathSegments(state.routePathSegments, routePathSegment)); + this._deleteRoutePathSegments( + routePathSegments, + this._normalizeRoutePathSegments( + state.routePathSegments, + routePathSegment, + ), + ); + } + + /** + * ** Normalize Route path segments. + */ + private _normalizeRoutePathSegments( + urlSegments: { [segmentId: string]: LiteralComponentsState }, + urlSegmentName: string, + ): LiteralComponentsState { + if (CollectionsUtil.isNil(urlSegments[urlSegmentName])) { + urlSegments[urlSegmentName] = { + components: {}, + routePathSegments: {}, + }; } - /** - * ** Delete all components state for a given route path segment. - */ - private _deleteRoutePathSegments(routePathSegments: string[], state: LiteralComponentsState): void { - const routePathSegment = routePathSegments.shift(); - - if (!routePathSegment) { - return; - } - - if (CollectionsUtil.isArrayEmpty(routePathSegments)) { - delete state.routePathSegments[routePathSegment]; - - return; - } - - this._deleteRoutePathSegments(routePathSegments, this._normalizeRoutePathSegments(state.routePathSegments, routePathSegment)); - } - - /** - * ** Normalize Route path segments. - */ - private _normalizeRoutePathSegments( - urlSegments: { [segmentId: string]: LiteralComponentsState }, - urlSegmentName: string - ): LiteralComponentsState { - if (CollectionsUtil.isNil(urlSegments[urlSegmentName])) { - urlSegments[urlSegmentName] = { - components: {}, - routePathSegments: {} - }; - } - - return urlSegments[urlSegmentName]; - } - - private _shallowCloneComponentsState(source: LiteralComponentsState, target?: LiteralComponentsState): LiteralComponentsState { - const _source: LiteralComponentsState = source ?? { components: {}, routePathSegments: {} }; - const _target: LiteralComponentsState = target ?? { components: {}, routePathSegments: {} }; - - CollectionsUtil.iterateObject(_source.components, (value, key) => { - _target.components[key] = value; - }); - - CollectionsUtil.iterateObject(_source.routePathSegments, (value, key) => { - _target.routePathSegments[key] = { - components: {}, - routePathSegments: {} - }; - - this._shallowCloneComponentsState(value, _target.routePathSegments[key]); - }); - - return _target; - } + return urlSegments[urlSegmentName]; + } + + private _shallowCloneComponentsState( + source: LiteralComponentsState, + target?: LiteralComponentsState, + ): LiteralComponentsState { + const _source: LiteralComponentsState = source ?? { + components: {}, + routePathSegments: {}, + }; + const _target: LiteralComponentsState = target ?? { + components: {}, + routePathSegments: {}, + }; + + CollectionsUtil.iterateObject(_source.components, (value, key) => { + _target.components[key] = value; + }); + + CollectionsUtil.iterateObject(_source.routePathSegments, (value, key) => { + _target.routePathSegments[key] = { + components: {}, + routePathSegments: {}, + }; + + this._shallowCloneComponentsState(value, _target.routePathSegments[key]); + }); + + return _target; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts index 599fd32ce0..6bdac7742e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component-status.model'; -export * from './component-state.model'; -export * from './components-state.model'; +export * from "./component-status.model"; +export * from "./component-state.model"; +export * from "./components-state.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts index f2e7aa03c3..fd47f7b89e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts @@ -5,736 +5,980 @@ /* eslint-disable @typescript-eslint/dot-notation,arrow-body-style,prefer-arrow/prefer-arrow-functions */ -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; -import { Store } from '@ngrx/store'; -import { RouterReducerState } from '@ngrx/router-store'; +import { Store } from "@ngrx/store"; +import { RouterReducerState } from "@ngrx/router-store"; -import { of, throwError } from 'rxjs'; +import { of, throwError } from "rxjs"; -import { marbles } from 'rxjs-marbles/jasmine'; +import { marbles } from "rxjs-marbles/jasmine"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { GenericAction, STORE_COMPONENTS, STORE_ROUTER, StoreState } from '../../ngrx'; -import { RouterService, RouterState, RouteSegments, RouteState } from '../../router'; +import { + GenericAction, + STORE_COMPONENTS, + STORE_ROUTER, + StoreState, +} from "../../ngrx"; +import { + RouterService, + RouterState, + RouteSegments, + RouteState, +} from "../../router"; import { - ComponentModel, - ComponentState, - ComponentStateImpl, - FAILED, - IDLE, - INITIALIZED, - LiteralComponentsState, - LiteralComponentState, - LOADED, - LOADING, - StatusType -} from '../model'; -import { ComponentIdle, ComponentInit, ComponentLoading, ComponentUpdate } from '../state'; - -import { ComponentService, ComponentServiceImpl } from './component.service'; - -describe('ComponentService -> ComponentServiceImpl', () => { - let storeStub$: jasmine.SpyObj>; - let routerServiceStub: jasmine.SpyObj; - - let service: ComponentService; + ComponentModel, + ComponentState, + ComponentStateImpl, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentsState, + LiteralComponentState, + LOADED, + LOADING, + StatusType, +} from "../model"; +import { + ComponentIdle, + ComponentInit, + ComponentLoading, + ComponentUpdate, +} from "../state"; + +import { ComponentService, ComponentServiceImpl } from "./component.service"; + +describe("ComponentService -> ComponentServiceImpl", () => { + let storeStub$: jasmine.SpyObj>; + let routerServiceStub: jasmine.SpyObj; + + let service: ComponentService; + + beforeEach(() => { + storeStub$ = jasmine.createSpyObj>("store", [ + "select", + "dispatch", + ]); + routerServiceStub = jasmine.createSpyObj("routerService", [ + "get", + "getState", + ]); + + TestBed.configureTestingModule({ + providers: [ + { provide: Store, useValue: storeStub$ }, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ComponentService, useClass: ComponentServiceImpl }, + ], + }); + + service = TestBed.inject(ComponentService); + }); + + it("should verify service is created", () => { + // Then + expect(service).toBeDefined(); + }); + + describe("Methods::", () => { + let id: string; + let components: LiteralComponentsState; + let router: RouterState; beforeEach(() => { - storeStub$ = jasmine.createSpyObj>('store', ['select', 'dispatch']); - routerServiceStub = jasmine.createSpyObj('routerService', ['get', 'getState']); - - TestBed.configureTestingModule({ - providers: [ - { provide: Store, useValue: storeStub$ }, - { provide: RouterService, useValue: routerServiceStub }, - { provide: ComponentService, useClass: ComponentServiceImpl } - ] - }); + id = "testComponent10"; + router = createStoreState()[STORE_ROUTER]; - service = TestBed.inject(ComponentService); + routerServiceStub.getState.and.returnValue(of(router.state)); + routerServiceStub.get.and.returnValue(of(router)); }); - it('should verify service is created', () => { - // Then - expect(service).toBeDefined(); - }); + describe("|init|", () => { + it("should verify will initialize correct component and dispatch ComponentInit", () => { + // Given + const componentState = createComponentState(router, id, INITIALIZED); - describe('Methods::', () => { - let id: string; - let components: LiteralComponentsState; - let router: RouterState; + storeStub$.select.and.callFake((param) => { + if (param === STORE_COMPONENTS) { + components = createStoreState(componentState.toLiteral())[ + STORE_COMPONENTS + ]; - beforeEach(() => { - id = 'testComponent10'; - router = createStoreState()[STORE_ROUTER]; + return of(components); + } - routerServiceStub.getState.and.returnValue(of(router.state)); - routerServiceStub.get.and.returnValue(of(router)); + return of(createStoreState()); }); - describe('|init|', () => { - it('should verify will initialize correct component and dispatch ComponentInit', () => { - // Given - const componentState = createComponentState(router, id, INITIALIZED); - - storeStub$.select.and.callFake((param) => { - if (param === STORE_COMPONENTS) { - components = createStoreState(componentState.toLiteral())[STORE_COMPONENTS]; + // When + const response$ = service.init(id, router.state); - return of(components); - } + // Then + expect(storeStub$.select.calls.argsFor(0)).toEqual([ + jasmine.any(Function), + ]); + expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + expect(storeStub$.dispatch).toHaveBeenCalledWith( + ComponentInit.of(componentState), + ); + + response$.subscribe((model) => { + expect(model["_routerState"]).toEqual(router); + expect(model["_componentState"]).toEqual( + getComponentState(components, id), + ); + expect(model.status).toEqual(INITIALIZED); + }); - return of(createStoreState()); - }); + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(storeStub$.select.calls.argsFor(2)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.get).toHaveBeenCalled(); + }, 1000); - // When - const response$ = service.init(id, router.state); + it("should verify wont dispatch ComponentInit", () => { + // Given + const componentState = createComponentState(router, id, LOADED); - // Then - expect(storeStub$.select.calls.argsFor(0)).toEqual([jasmine.any(Function)]); - expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); - expect(storeStub$.dispatch).toHaveBeenCalledWith(ComponentInit.of(componentState)); + storeStub$.select.and.callFake((param) => { + if (param === STORE_COMPONENTS) { + components = createStoreState(componentState.toLiteral())[ + STORE_COMPONENTS + ]; - response$.subscribe((model) => { - expect(model['_routerState']).toEqual(router); - expect(model['_componentState']).toEqual(getComponentState(components, id)); - expect(model.status).toEqual(INITIALIZED); - }); + return of(components); + } - expect(routerServiceStub.getState).toHaveBeenCalled(); - expect(storeStub$.select.calls.argsFor(2)).toEqual([STORE_COMPONENTS]); - expect(routerServiceStub.get).toHaveBeenCalled(); - }, 1000); + return of(createStoreState(componentState.toLiteral())); + }); - it('should verify wont dispatch ComponentInit', () => { - // Given - const componentState = createComponentState(router, id, LOADED); + // When + const response$ = service.init(id, router.state); - storeStub$.select.and.callFake((param) => { - if (param === STORE_COMPONENTS) { - components = createStoreState(componentState.toLiteral())[STORE_COMPONENTS]; + // Then + expect(storeStub$.select.calls.argsFor(0)).toEqual([ + jasmine.any(Function), + ]); + expect(storeStub$.dispatch).not.toHaveBeenCalled(); + expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + + response$.subscribe((model) => { + expect(model["_routerState"]).toEqual(router); + expect(model["_componentState"]).toEqual( + getComponentState(components, id), + ); + expect(model.status).toEqual(LOADED); + }); - return of(components); - } + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(storeStub$.select.calls.argsFor(2)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.get).toHaveBeenCalled(); + }, 1000); + }); - return of(createStoreState(componentState.toLiteral())); - }); + describe("|idle|", () => { + it("should verify will dispatch ComponentIdle with provided ComponentState", () => { + // Given + const componentState = createComponentState(router, id, LOADED); - // When - const response$ = service.init(id, router.state); + // When + service.idle(componentState); - // Then - expect(storeStub$.select.calls.argsFor(0)).toEqual([jasmine.any(Function)]); - expect(storeStub$.dispatch).not.toHaveBeenCalled(); - expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + // Then + expect(storeStub$.dispatch).toHaveBeenCalledWith( + ComponentIdle.of(componentState), + ); + }); + }); - response$.subscribe((model) => { - expect(model['_routerState']).toEqual(router); - expect(model['_componentState']).toEqual(getComponentState(components, id)); - expect(model.status).toEqual(LOADED); - }); + describe("|load|", () => { + it("should verify will dispatch ComponentLoading with provided ComponentState", () => { + // Given + const componentState = createComponentState(router, id, FAILED); + routerServiceStub.get.and.returnValue(of(router)); + storeStub$.select.and.returnValue( + of( + createStoreState( + createComponentState(router, id, LOADED).toLiteral(), + )[STORE_COMPONENTS], + ), + ); + + // When + const response$ = service.load(componentState); - expect(routerServiceStub.getState).toHaveBeenCalled(); - expect(storeStub$.select.calls.argsFor(2)).toEqual([STORE_COMPONENTS]); - expect(routerServiceStub.get).toHaveBeenCalled(); - }, 1000); + // Then + expect(routerServiceStub.get).toHaveBeenCalledTimes(1); + // @ts-ignore + expect(storeStub$.dispatch.calls.argsFor(0)).toEqual([ + ComponentLoading.of( + componentState.copy({ + navigationId: router.navigationId, + status: LOADING, + }), + ), + ]); + + const assertionComponentState = createComponentState( + router, + id, + LOADED, + ); + response$.subscribe((model) => { + expect(model["_routerState"]).toEqual(router); + expect(model["_componentState"]).toEqual(assertionComponentState); + expect(model.status).toEqual(LOADED); }); - describe('|idle|', () => { - it('should verify will dispatch ComponentIdle with provided ComponentState', () => { - // Given - const componentState = createComponentState(router, id, LOADED); - - // When - service.idle(componentState); + // @ts-ignore + expect(storeStub$.select).toHaveBeenCalledWith(STORE_COMPONENTS); + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(routerServiceStub.get).toHaveBeenCalledTimes(2); + }, 1000); + + it("should verify will dispatch ComponentIdle and ComponentLoading with provided component id", () => { + // Given + const componentState = createComponentState(router, id, INITIALIZED); + routerServiceStub.get.and.returnValue(of(router)); + storeStub$.select.and.returnValue( + of( + createStoreState( + createComponentState(router, id, LOADED).toLiteral(), + )[STORE_COMPONENTS], + ), + ); + + // When + const response$ = service.load(componentState); - // Then - expect(storeStub$.dispatch).toHaveBeenCalledWith(ComponentIdle.of(componentState)); - }); + // Then + expect(routerServiceStub.get).toHaveBeenCalledTimes(1); + // @ts-ignore + expect(storeStub$.dispatch.calls.argsFor(0)).toEqual([ + ComponentIdle.of( + createComponentState(router, id, IDLE).copy({ + navigationId: router.navigationId, + }), + ), + ]); + expect(storeStub$.dispatch.calls.argsFor(1)).toEqual([ + ComponentLoading.of( + createComponentState(router, id, LOADING).copy({ + navigationId: router.navigationId, + }), + ), + ]); + + const assertionComponentState = createComponentState( + router, + id, + LOADED, + ); + response$.subscribe((model) => { + expect(model["_routerState"]).toEqual(router); + expect(model["_componentState"]).toEqual(assertionComponentState); + expect(model.status).toEqual(LOADED); }); - describe('|load|', () => { - it('should verify will dispatch ComponentLoading with provided ComponentState', () => { - // Given - const componentState = createComponentState(router, id, FAILED); - routerServiceStub.get.and.returnValue(of(router)); - storeStub$.select.and.returnValue( - of(createStoreState(createComponentState(router, id, LOADED).toLiteral())[STORE_COMPONENTS]) - ); - - // When - const response$ = service.load(componentState); - - // Then - expect(routerServiceStub.get).toHaveBeenCalledTimes(1); - // @ts-ignore - expect(storeStub$.dispatch.calls.argsFor(0)).toEqual([ - ComponentLoading.of( - componentState.copy({ - navigationId: router.navigationId, - status: LOADING - }) - ) - ]); - - const assertionComponentState = createComponentState(router, id, LOADED); - response$.subscribe((model) => { - expect(model['_routerState']).toEqual(router); - expect(model['_componentState']).toEqual(assertionComponentState); - expect(model.status).toEqual(LOADED); - }); - - // @ts-ignore - expect(storeStub$.select).toHaveBeenCalledWith(STORE_COMPONENTS); - expect(routerServiceStub.getState).toHaveBeenCalled(); - expect(routerServiceStub.get).toHaveBeenCalledTimes(2); - }, 1000); - - it('should verify will dispatch ComponentIdle and ComponentLoading with provided component id', () => { - // Given - const componentState = createComponentState(router, id, INITIALIZED); - routerServiceStub.get.and.returnValue(of(router)); - storeStub$.select.and.returnValue( - of(createStoreState(createComponentState(router, id, LOADED).toLiteral())[STORE_COMPONENTS]) - ); - - // When - const response$ = service.load(componentState); - - // Then - expect(routerServiceStub.get).toHaveBeenCalledTimes(1); - // @ts-ignore - expect(storeStub$.dispatch.calls.argsFor(0)).toEqual([ - ComponentIdle.of( - createComponentState(router, id, IDLE).copy({ - navigationId: router.navigationId - }) - ) - ]); - expect(storeStub$.dispatch.calls.argsFor(1)).toEqual([ - ComponentLoading.of( - createComponentState(router, id, LOADING).copy({ - navigationId: router.navigationId - }) - ) - ]); - - const assertionComponentState = createComponentState(router, id, LOADED); - response$.subscribe((model) => { - expect(model['_routerState']).toEqual(router); - expect(model['_componentState']).toEqual(assertionComponentState); - expect(model.status).toEqual(LOADED); - }); - - // @ts-ignore - expect(storeStub$.select).toHaveBeenCalledWith(STORE_COMPONENTS); - expect(routerServiceStub.getState).toHaveBeenCalled(); - expect(routerServiceStub.get).toHaveBeenCalledTimes(2); - }, 1000); - }); + // @ts-ignore + expect(storeStub$.select).toHaveBeenCalledWith(STORE_COMPONENTS); + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(routerServiceStub.get).toHaveBeenCalledTimes(2); + }, 1000); + }); - describe('|update|', () => { - it('should verify will dispatch ComponentUpdate with provided ComponentState', () => { - // Given - const componentState = createComponentState(router, id, LOADED); + describe("|update|", () => { + it("should verify will dispatch ComponentUpdate with provided ComponentState", () => { + // Given + const componentState = createComponentState(router, id, LOADED); - // When - service.update(componentState); + // When + service.update(componentState); - // Then - expect(routerServiceStub.get).toHaveBeenCalled(); - expect(storeStub$.dispatch).toHaveBeenCalledWith( - ComponentUpdate.of(componentState.copy({ navigationId: router.navigationId })) - ); - }); - }); + // Then + expect(routerServiceStub.get).toHaveBeenCalled(); + expect(storeStub$.dispatch).toHaveBeenCalledWith( + ComponentUpdate.of( + componentState.copy({ navigationId: router.navigationId }), + ), + ); + }); + }); - describe('|hasInSegment|', () => { - it( - 'should verify will return true when ComponentState exist', - marbles((m) => { - // Given - const componentsStream$ = m.cold('---a', { - a: createStoreState(createComponentState(router, 'someComponentId', LOADED).toLiteral())[STORE_COMPONENTS] - }); - const routeStateStream$ = m.cold('--a', { - a: router.state - }); - storeStub$.select.and.returnValue(componentsStream$); - routerServiceStub.getState.and.returnValue(routeStateStream$); - const expected$ = m.cold('---(a|)', { a: true }); - - // When - const response$ = service.hasInSegment('someComponentId', ['test_domain/context', 'test_entity/10']); - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - - it( - 'should verify will return false when ComponentState not exist', - marbles((m) => { - // Given - const componentsStream$ = m.cold('---a', { - a: createStoreState(createComponentState(router, 'someComponentId', LOADED).toLiteral())[STORE_COMPONENTS] - }); - const routeStateStream$ = m.cold('---a', { - a: router.state - }); - storeStub$.select.and.returnValue(componentsStream$); - routerServiceStub.getState.and.returnValue(routeStateStream$); - const expected$ = m.cold('---(a|)', { a: false }); - - // When - const response$ = service.hasInSegment('newComponentId', ['test_domain/context', 'test_entity/10']); - - // Then - m.expect(response$).toBeObservable(expected$); - }) - ); - }); + describe("|hasInSegment|", () => { + it( + "should verify will return true when ComponentState exist", + marbles((m) => { + // Given + const componentsStream$ = m.cold("---a", { + a: createStoreState( + createComponentState( + router, + "someComponentId", + LOADED, + ).toLiteral(), + )[STORE_COMPONENTS], + }); + const routeStateStream$ = m.cold("--a", { + a: router.state, + }); + storeStub$.select.and.returnValue(componentsStream$); + routerServiceStub.getState.and.returnValue(routeStateStream$); + const expected$ = m.cold("---(a|)", { a: true }); + + // When + const response$ = service.hasInSegment("someComponentId", [ + "test_domain/context", + "test_entity/10", + ]); + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + + it( + "should verify will return false when ComponentState not exist", + marbles((m) => { + // Given + const componentsStream$ = m.cold("---a", { + a: createStoreState( + createComponentState( + router, + "someComponentId", + LOADED, + ).toLiteral(), + )[STORE_COMPONENTS], + }); + const routeStateStream$ = m.cold("---a", { + a: router.state, + }); + storeStub$.select.and.returnValue(componentsStream$); + routerServiceStub.getState.and.returnValue(routeStateStream$); + const expected$ = m.cold("---(a|)", { a: false }); + + // When + const response$ = service.hasInSegment("newComponentId", [ + "test_domain/context", + "test_entity/10", + ]); + + // Then + m.expect(response$).toBeObservable(expected$); + }), + ); + }); - describe('|onInit|', () => { - it( - `should verify wont emit when state doesn't exist`, - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, 'testId10', INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, 'testId10', IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, 'testId10', LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, 'testId10', LOADED).toLiteral()); - const cE = createStoreState(createComponentState(router, 'testId22', LOADED).toLiteral()); - const cF = createStoreState(createComponentState(router, 'testId33', LOADED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc-d-e--f-|', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS], - e: cE[STORE_COMPONENTS], - f: cF[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('--------------|'); - storeStub$.select.and.returnValue(componentsMarbleStream$); - - // When - const response$ = service.onInit(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it('should verify will emit when state exist', () => { - // Given - const componentState = createComponentState(router, id, INITIALIZED); - const storeState = createStoreState(componentState.toLiteral()); - - storeStub$.select.and.returnValue(of(storeState[STORE_COMPONENTS])); - - // When - const response$ = service.onInit(id, router.state.routePathSegments); - - // Then - // @ts-ignore - expect(storeStub$.select.calls.argsFor(0)).toEqual([STORE_COMPONENTS]); - expect(routerServiceStub.getState).toHaveBeenCalled(); - - response$.subscribe((model) => { - expect(model).toBeDefined(); - expect(model.status).toEqual(INITIALIZED); - }); - - expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); - expect(routerServiceStub.get).toHaveBeenCalled(); - }, 1000); - }); + describe("|onInit|", () => { + it( + `should verify wont emit when state doesn't exist`, + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, "testId10", INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, "testId10", IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, "testId10", LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, "testId10", LOADED).toLiteral(), + ); + const cE = createStoreState( + createComponentState(router, "testId22", LOADED).toLiteral(), + ); + const cF = createStoreState( + createComponentState(router, "testId33", LOADED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc-d-e--f-|", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("--------------|"); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.onInit(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it("should verify will emit when state exist", () => { + // Given + const componentState = createComponentState(router, id, INITIALIZED); + const storeState = createStoreState(componentState.toLiteral()); + + storeStub$.select.and.returnValue(of(storeState[STORE_COMPONENTS])); + + // When + const response$ = service.onInit(id, router.state.routePathSegments); - describe('|onLoaded|', () => { - it( - `should verify wont emit when state doesn't exist`, - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, 'testId1', INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, 'testId1', IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, 'testId1', LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, 'testId1', LOADED).toLiteral()); - const cE = createStoreState(createComponentState(router, 'testId2', LOADED).toLiteral()); - const cF = createStoreState(createComponentState(router, 'testId3', LOADED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---d--e----f-|', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS], - e: cE[STORE_COMPONENTS], - f: cF[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('-------------------|'); - storeStub$.select.and.returnValue(componentsMarbleStream$); - - // When - const response$ = service.onLoaded(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it( - 'should verify wont emit when state status is not LOADED or FAILED', - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---|', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('---------|'); - storeStub$.select.and.returnValue(componentsMarbleStream$); - - // When - const response$ = service.onLoaded(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it( - 'should verify will emit when state exist and its status is LOADED', - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---d---', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS] - }); - const componentsMarbleStream1$ = m.cold('d----', { - d: cD[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('---------(a|)', { - a: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]) - }); - let cnt = 0; - storeStub$.select.and.callFake(() => { - cnt++; - if (cnt === 1) { - return componentsMarbleStream$; - } - - if (cnt === 2) { - return componentsMarbleStream1$; - } - - return throwError(() => new Error(`This shouldn't be reachable`)); - }); - routerServiceStub.getState.and.returnValue(m.cold('a----', { a: router.state })); - routerServiceStub.get.and.returnValue(m.cold('a----', { a: router })); - - // When - const response$ = service.onLoaded(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it( - 'should verify will emit when state exist and its status is FAILED', - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, id, FAILED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---d---', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS] - }); - const componentsMarbleStream1$ = m.cold('d----', { - d: cD[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('---------(a|)', { - a: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]) - }); - let cnt = 0; - storeStub$.select.and.callFake(() => { - cnt++; - if (cnt === 1) { - return componentsMarbleStream$; - } - - if (cnt === 2) { - return componentsMarbleStream1$; - } - - return throwError(() => new Error(`This shouldn't be reachable`)); - }); - routerServiceStub.getState.and.returnValue(m.cold('a----', { a: router.state })); - routerServiceStub.get.and.returnValue(m.cold('a----', { a: router })); - - // When - const response$ = service.onLoaded(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - }); + // Then + // @ts-ignore + expect(storeStub$.select.calls.argsFor(0)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.getState).toHaveBeenCalled(); - describe('|getModel|', () => { - it( - `should verify wont emit when state doesn't exist`, - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, 'testId21', INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, 'testId21', IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, 'testId21', LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, 'testId21', LOADED).toLiteral()); - const cE = createStoreState(createComponentState(router, 'testId32', LOADED).toLiteral()); - const cF = createStoreState(createComponentState(router, 'testId33', LOADED).toLiteral()); - const componentsMarbleStream$ = m.cold('--a-bc---d----e--f---|', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS], - e: cE[STORE_COMPONENTS], - f: cF[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('---------------------|'); - storeStub$.select.and.returnValue(componentsMarbleStream$); - - // When - const response$ = service.getModel(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it( - 'should verify will emit on every status', - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); - const cE = createStoreState(createComponentState(router, id, FAILED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---d---e-', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS], - e: cE[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('-a--bc---d---e-', { - a: ComponentModel.of(getComponentState(cA, id), cA[STORE_ROUTER]), - b: ComponentModel.of(getComponentState(cB, id), cB[STORE_ROUTER]), - c: ComponentModel.of(getComponentState(cC, id), cC[STORE_ROUTER]), - d: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), - e: ComponentModel.of(getComponentState(cE, id), cE[STORE_ROUTER]) - }); - storeStub$.select.and.returnValue(componentsMarbleStream$); - routerServiceStub.get.and.returnValue(m.cold('a', { a: router })); - - // When - const response$ = service.getModel(id, router.state.routePathSegments, ['*']); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it( - 'should verify will emit on requested statuses', - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); - const cE = createStoreState(createComponentState(router, id, FAILED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---d---e-', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS], - e: cE[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('-----c-------e-', { - c: ComponentModel.of(getComponentState(cC, id), cC[STORE_ROUTER]), - e: ComponentModel.of(getComponentState(cE, id), cE[STORE_ROUTER]) - }); - storeStub$.select.and.returnValue(componentsMarbleStream$); - routerServiceStub.get.and.returnValue(m.cold('a', { a: router })); - - // When - const response$ = service.getModel(id, router.state.routePathSegments, [LOADING, FAILED]); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); - - it( - 'should verify will emit on default statuses', - marbles((m) => { - // Given - const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); - const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); - const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); - const cE = createStoreState(createComponentState(router, id, LOADING).toLiteral()); - const cF = createStoreState(createComponentState(router, id, FAILED).toLiteral()); - const componentsMarbleStream$ = m.cold('-a--bc---d---e---f-', { - a: cA[STORE_COMPONENTS], - b: cB[STORE_COMPONENTS], - c: cC[STORE_COMPONENTS], - d: cD[STORE_COMPONENTS], - e: cE[STORE_COMPONENTS], - f: cF[STORE_COMPONENTS] - }); - const expectedModelMarbleStream$ = m.cold('---------d-------f-', { - d: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), - f: ComponentModel.of(getComponentState(cF, id), cF[STORE_ROUTER]) - }); - storeStub$.select.and.returnValue(componentsMarbleStream$); - routerServiceStub.get.and.returnValue(m.cold('a', { a: router })); - - // When - const response$ = service.getModel(id, router.state.routePathSegments); - - // Then - m.expect(response$).toBeObservable(expectedModelMarbleStream$); - }) - ); + response$.subscribe((model) => { + expect(model).toBeDefined(); + expect(model.status).toEqual(INITIALIZED); }); - describe('|dispatchAction|', () => { - it('should verify will invoke correct methods', () => { - // Given - const componentState = createComponentState(router, id, LOADING); - const typeStub = '[component] Some Action'; - const modelStub = { - getComponentState: () => componentState - } as ComponentModel; - const getModelSpy = spyOn(service, 'getModel').and.returnValue(of(modelStub)); - const actionStub = { type: typeStub, payload: componentState, task: undefined }; - const genericActionOfSpy = spyOn(GenericAction, 'of').and.returnValue(actionStub); - - // When - service.dispatchAction(typeStub, componentState); - - // Then - expect(getModelSpy).toHaveBeenCalledWith(componentState.id, componentState.routePathSegments, ['*']); - expect(genericActionOfSpy).toHaveBeenCalledWith(typeStub, componentState, undefined); - expect(storeStub$.dispatch).toHaveBeenCalledWith(actionStub); - }); - - it('should verify will invoke correct methods when Task is provided', () => { - // Given - const taskIdentifier = `search_data __ ${new Date().toISOString()}`; - const componentState = createComponentState(router, id, INITIALIZED); - const typeStub = '[component] Load Users'; - const modelStub = { - getComponentState: () => componentState - } as ComponentModel; - const getModelSpy = spyOn(service, 'getModel').and.returnValue(of(modelStub)); - const actionStub = { - type: typeStub, - payload: componentState, - task: taskIdentifier - }; - const genericActionOfSpy = spyOn(GenericAction, 'of').and.returnValue(actionStub); - - // When - service.dispatchAction(typeStub, componentState, taskIdentifier); - - // Then - expect(getModelSpy).toHaveBeenCalledWith(componentState.id, componentState.routePathSegments, ['*']); - expect(genericActionOfSpy).toHaveBeenCalledWith(typeStub, componentState, taskIdentifier); - expect(storeStub$.dispatch).toHaveBeenCalledWith(actionStub); - }); - }); + expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.get).toHaveBeenCalled(); + }, 1000); }); -}); -const createComponentState = (router: RouterReducerState, id: string, status: StatusType) => { - return ComponentStateImpl.of({ - id, - status, - routePath: router?.state.routePath ?? null, - routePathSegments: router?.state.routePathSegments ?? [], - navigationId: router.navigationId + describe("|onLoaded|", () => { + it( + `should verify wont emit when state doesn't exist`, + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, "testId1", INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, "testId1", IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, "testId1", LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, "testId1", LOADED).toLiteral(), + ); + const cE = createStoreState( + createComponentState(router, "testId2", LOADED).toLiteral(), + ); + const cF = createStoreState( + createComponentState(router, "testId3", LOADED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---d--e----f-|", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("-------------------|"); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.onLoaded( + id, + router.state.routePathSegments, + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it( + "should verify wont emit when state status is not LOADED or FAILED", + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, id, INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, id, IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---|", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("---------|"); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.onLoaded( + id, + router.state.routePathSegments, + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it( + "should verify will emit when state exist and its status is LOADED", + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, id, INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, id, IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, id, LOADED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---d---", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + }); + const componentsMarbleStream1$ = m.cold("d----", { + d: cD[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("---------(a|)", { + a: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), + }); + let cnt = 0; + storeStub$.select.and.callFake(() => { + cnt++; + if (cnt === 1) { + return componentsMarbleStream$; + } + + if (cnt === 2) { + return componentsMarbleStream1$; + } + + return throwError(() => new Error(`This shouldn't be reachable`)); + }); + routerServiceStub.getState.and.returnValue( + m.cold("a----", { a: router.state }), + ); + routerServiceStub.get.and.returnValue(m.cold("a----", { a: router })); + + // When + const response$ = service.onLoaded( + id, + router.state.routePathSegments, + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it( + "should verify will emit when state exist and its status is FAILED", + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, id, INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, id, IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, id, FAILED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---d---", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + }); + const componentsMarbleStream1$ = m.cold("d----", { + d: cD[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("---------(a|)", { + a: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), + }); + let cnt = 0; + storeStub$.select.and.callFake(() => { + cnt++; + if (cnt === 1) { + return componentsMarbleStream$; + } + + if (cnt === 2) { + return componentsMarbleStream1$; + } + + return throwError(() => new Error(`This shouldn't be reachable`)); + }); + routerServiceStub.getState.and.returnValue( + m.cold("a----", { a: router.state }), + ); + routerServiceStub.get.and.returnValue(m.cold("a----", { a: router })); + + // When + const response$ = service.onLoaded( + id, + router.state.routePathSegments, + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + }); + + describe("|getModel|", () => { + it( + `should verify wont emit when state doesn't exist`, + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, "testId21", INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, "testId21", IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, "testId21", LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, "testId21", LOADED).toLiteral(), + ); + const cE = createStoreState( + createComponentState(router, "testId32", LOADED).toLiteral(), + ); + const cF = createStoreState( + createComponentState(router, "testId33", LOADED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("--a-bc---d----e--f---|", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("---------------------|"); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.getModel( + id, + router.state.routePathSegments, + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it( + "should verify will emit on every status", + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, id, INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, id, IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, id, LOADED).toLiteral(), + ); + const cE = createStoreState( + createComponentState(router, id, FAILED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---d---e-", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("-a--bc---d---e-", { + a: ComponentModel.of(getComponentState(cA, id), cA[STORE_ROUTER]), + b: ComponentModel.of(getComponentState(cB, id), cB[STORE_ROUTER]), + c: ComponentModel.of(getComponentState(cC, id), cC[STORE_ROUTER]), + d: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), + e: ComponentModel.of(getComponentState(cE, id), cE[STORE_ROUTER]), + }); + storeStub$.select.and.returnValue(componentsMarbleStream$); + routerServiceStub.get.and.returnValue(m.cold("a", { a: router })); + + // When + const response$ = service.getModel( + id, + router.state.routePathSegments, + ["*"], + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it( + "should verify will emit on requested statuses", + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, id, INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, id, IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, id, LOADED).toLiteral(), + ); + const cE = createStoreState( + createComponentState(router, id, FAILED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---d---e-", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("-----c-------e-", { + c: ComponentModel.of(getComponentState(cC, id), cC[STORE_ROUTER]), + e: ComponentModel.of(getComponentState(cE, id), cE[STORE_ROUTER]), + }); + storeStub$.select.and.returnValue(componentsMarbleStream$); + routerServiceStub.get.and.returnValue(m.cold("a", { a: router })); + + // When + const response$ = service.getModel( + id, + router.state.routePathSegments, + [LOADING, FAILED], + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); + + it( + "should verify will emit on default statuses", + marbles((m) => { + // Given + const cA = createStoreState( + createComponentState(router, id, INITIALIZED).toLiteral(), + ); + const cB = createStoreState( + createComponentState(router, id, IDLE).toLiteral(), + ); + const cC = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const cD = createStoreState( + createComponentState(router, id, LOADED).toLiteral(), + ); + const cE = createStoreState( + createComponentState(router, id, LOADING).toLiteral(), + ); + const cF = createStoreState( + createComponentState(router, id, FAILED).toLiteral(), + ); + const componentsMarbleStream$ = m.cold("-a--bc---d---e---f-", { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS], + }); + const expectedModelMarbleStream$ = m.cold("---------d-------f-", { + d: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), + f: ComponentModel.of(getComponentState(cF, id), cF[STORE_ROUTER]), + }); + storeStub$.select.and.returnValue(componentsMarbleStream$); + routerServiceStub.get.and.returnValue(m.cold("a", { a: router })); + + // When + const response$ = service.getModel( + id, + router.state.routePathSegments, + ); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + }), + ); }); -}; -const getComponentState = (state: StoreState | LiteralComponentsState, id: string): ComponentState => { - if (CollectionsUtil.isNil(state)) { - return ComponentStateImpl.of({}); - } + describe("|dispatchAction|", () => { + it("should verify will invoke correct methods", () => { + // Given + const componentState = createComponentState(router, id, LOADING); + const typeStub = "[component] Some Action"; + const modelStub = { + getComponentState: () => componentState, + } as ComponentModel; + const getModelSpy = spyOn(service, "getModel").and.returnValue( + of(modelStub), + ); + const actionStub = { + type: typeStub, + payload: componentState, + task: undefined, + }; + const genericActionOfSpy = spyOn(GenericAction, "of").and.returnValue( + actionStub, + ); + + // When + service.dispatchAction(typeStub, componentState); - const componentsState = ( - CollectionsUtil.isNil((state as LiteralComponentsState).routePathSegments) ? state[STORE_COMPONENTS] : state - ) as LiteralComponentsState; + // Then + expect(getModelSpy).toHaveBeenCalledWith( + componentState.id, + componentState.routePathSegments, + ["*"], + ); + expect(genericActionOfSpy).toHaveBeenCalledWith( + typeStub, + componentState, + undefined, + ); + expect(storeStub$.dispatch).toHaveBeenCalledWith(actionStub); + }); + + it("should verify will invoke correct methods when Task is provided", () => { + // Given + const taskIdentifier = `search_data __ ${new Date().toISOString()}`; + const componentState = createComponentState(router, id, INITIALIZED); + const typeStub = "[component] Load Users"; + const modelStub = { + getComponentState: () => componentState, + } as ComponentModel; + const getModelSpy = spyOn(service, "getModel").and.returnValue( + of(modelStub), + ); + const actionStub = { + type: typeStub, + payload: componentState, + task: taskIdentifier, + }; + const genericActionOfSpy = spyOn(GenericAction, "of").and.returnValue( + actionStub, + ); + + // When + service.dispatchAction(typeStub, componentState, taskIdentifier); - return ComponentStateImpl.fromLiteralComponentState( - componentsState.routePathSegments['test_domain/context'].routePathSegments['test_entity/10'].components[id] - ); + // Then + expect(getModelSpy).toHaveBeenCalledWith( + componentState.id, + componentState.routePathSegments, + ["*"], + ); + expect(genericActionOfSpy).toHaveBeenCalledWith( + typeStub, + componentState, + taskIdentifier, + ); + expect(storeStub$.dispatch).toHaveBeenCalledWith(actionStub); + }); + }); + }); +}); + +const createComponentState = ( + router: RouterReducerState, + id: string, + status: StatusType, +) => { + return ComponentStateImpl.of({ + id, + status, + routePath: router?.state.routePath ?? null, + routePathSegments: router?.state.routePathSegments ?? [], + navigationId: router.navigationId, + }); +}; + +const getComponentState = ( + state: StoreState | LiteralComponentsState, + id: string, +): ComponentState => { + if (CollectionsUtil.isNil(state)) { + return ComponentStateImpl.of({}); + } + + const componentsState = ( + CollectionsUtil.isNil((state as LiteralComponentsState).routePathSegments) + ? state[STORE_COMPONENTS] + : state + ) as LiteralComponentsState; + + return ComponentStateImpl.fromLiteralComponentState( + componentsState.routePathSegments["test_domain/context"].routePathSegments[ + "test_entity/10" + ].components[id], + ); }; const createStoreState = (state?: LiteralComponentState): StoreState => { - return CollectionsUtil.isNil(state) - ? { - [STORE_ROUTER]: RouterState.of( - RouteState.of( - RouteSegments.of('test_entity/10', {}, { entity: 10 }, null, RouteSegments.of('test_domain/context')), - '/domain/context/entity/10' - ), - 3 - ), - [STORE_COMPONENTS]: { - components: {}, - routePathSegments: {} - } - } - : { - [STORE_ROUTER]: RouterState.of( - RouteState.of( - RouteSegments.of('test_entity/10', {}, { entity: 10 }, null, RouteSegments.of('test_domain/context')), - '/domain/context/entity/10' - ), - 3 - ), - [STORE_COMPONENTS]: { - components: {}, - routePathSegments: { - 'test_domain/context': { - components: {}, - routePathSegments: { - 'test_entity/10': { - components: { - [state.id]: state - }, - routePathSegments: {} - } - } - } - } - } - }; + return CollectionsUtil.isNil(state) + ? { + [STORE_ROUTER]: RouterState.of( + RouteState.of( + RouteSegments.of( + "test_entity/10", + {}, + { entity: 10 }, + null, + RouteSegments.of("test_domain/context"), + ), + "/domain/context/entity/10", + ), + 3, + ), + [STORE_COMPONENTS]: { + components: {}, + routePathSegments: {}, + }, + } + : { + [STORE_ROUTER]: RouterState.of( + RouteState.of( + RouteSegments.of( + "test_entity/10", + {}, + { entity: 10 }, + null, + RouteSegments.of("test_domain/context"), + ), + "/domain/context/entity/10", + ), + 3, + ), + [STORE_COMPONENTS]: { + components: {}, + routePathSegments: { + "test_domain/context": { + components: {}, + routePathSegments: { + "test_entity/10": { + components: { + [state.id]: state, + }, + routePathSegments: {}, + }, + }, + }, + }, + }, + }; }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts index 601167cab8..e50598969f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts @@ -5,91 +5,113 @@ /* eslint-disable @typescript-eslint/unified-signatures,ngrx/avoid-mapping-selectors */ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; -import { Observable } from 'rxjs'; -import { filter, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { Observable } from "rxjs"; +import { filter, map, switchMap, take, withLatestFrom } from "rxjs/operators"; -import { Store } from '@ngrx/store'; +import { Store } from "@ngrx/store"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { RouterService, RouterState, RouteState } from '../../router'; -import { GenericAction, STORE_COMPONENTS, StoreState } from '../../ngrx'; +import { RouterService, RouterState, RouteState } from "../../router"; +import { GenericAction, STORE_COMPONENTS, StoreState } from "../../ngrx"; -import { ComponentIdle, ComponentInit, ComponentLoading, ComponentUpdate } from '../state'; import { - ComponentModel, - ComponentsStateHelper, - ComponentState, - ComponentStateImpl, - FAILED, - IDLE, - INITIALIZED, - LiteralComponentsState, - LOADED, - LOADING, - StatusType -} from '../model'; + ComponentIdle, + ComponentInit, + ComponentLoading, + ComponentUpdate, +} from "../state"; +import { + ComponentModel, + ComponentsStateHelper, + ComponentState, + ComponentStateImpl, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentsState, + LOADED, + LOADING, + StatusType, +} from "../model"; /** * ** Service that manage Components State. */ export abstract class ComponentService { - /** - * ** Initialize Component State and return Model. - */ - abstract init(id: string, routeState: RouteState): Observable; - - /** - * ** Set Component status to IDLE. - */ - abstract idle(componentState: ComponentState): void; - - /** - * ** Load Component State and return Model. - */ - abstract load(componentState: ComponentState): Observable; - - /** - * ** Update Component State. - */ - abstract update(componentState: ComponentState): void; - - /** - * ** Acknowledge if has ComponentState in segment. - * - * - true - has ComponentState. - * - false - doesn't have ComponentState. - */ - abstract hasInSegment(id: string, routePathSegments: string[]): Observable; - - /** - * ** Listener that fires once after successful Component State initialization and returns Model. - */ - abstract onInit(id: string, routePathSegments: string[]): Observable; - - /** - * ** Listener that fires once after successful Component State load and returns Model. - */ - abstract onLoaded(id: string, routePathSegments: string[]): Observable; - - /** - * ** Returns stream with value Component Model and fires whenever Component State changes in Store. - * - * - If no statusWatch provided by default will listen for statuses {@link LOADED} and {@link FAILED}. - */ - abstract getModel(id: string, routePathSegments: string[], statusWatch?: Array): Observable; - - /** - * ** Dispatch GenericAction with provided Type, ComponentState and optionally task. - */ - abstract dispatchAction(type: string, componentState: ComponentState, task?: string): void; - - /** - * ** Initialize Service. - */ - abstract initialize(): void; + /** + * ** Initialize Component State and return Model. + */ + abstract init(id: string, routeState: RouteState): Observable; + + /** + * ** Set Component status to IDLE. + */ + abstract idle(componentState: ComponentState): void; + + /** + * ** Load Component State and return Model. + */ + abstract load(componentState: ComponentState): Observable; + + /** + * ** Update Component State. + */ + abstract update(componentState: ComponentState): void; + + /** + * ** Acknowledge if has ComponentState in segment. + * + * - true - has ComponentState. + * - false - doesn't have ComponentState. + */ + abstract hasInSegment( + id: string, + routePathSegments: string[], + ): Observable; + + /** + * ** Listener that fires once after successful Component State initialization and returns Model. + */ + abstract onInit( + id: string, + routePathSegments: string[], + ): Observable; + + /** + * ** Listener that fires once after successful Component State load and returns Model. + */ + abstract onLoaded( + id: string, + routePathSegments: string[], + ): Observable; + + /** + * ** Returns stream with value Component Model and fires whenever Component State changes in Store. + * + * - If no statusWatch provided by default will listen for statuses {@link LOADED} and {@link FAILED}. + */ + abstract getModel( + id: string, + routePathSegments: string[], + statusWatch?: Array, + ): Observable; + + /** + * ** Dispatch GenericAction with provided Type, ComponentState and optionally task. + */ + abstract dispatchAction( + type: string, + componentState: ComponentState, + task?: string, + ): void; + + /** + * ** Initialize Service. + */ + abstract initialize(): void; } /** @@ -97,262 +119,328 @@ export abstract class ComponentService { */ @Injectable() export class ComponentServiceImpl extends ComponentService { - private readonly componentsStateHelper: ComponentsStateHelper; - - /** - * ** Constructor. - */ - constructor( - private readonly store$: Store, - private readonly routerService: RouterService - ) { - super(); - - this.componentsStateHelper = new ComponentsStateHelper(); - } - - /** - * @inheritDoc - */ - init(id: string, routeState: RouteState): Observable { - this.store$ - .select((store) => store) - .pipe( - take(1), - map((store) => this._getComponentState(id, routeState.routePathSegments, store.router, store.components)) - ) - .subscribe((componentState) => { - if (componentState.status === INITIALIZED) { - this.store$.dispatch(ComponentInit.of(componentState)); - } - }); - - return this.onInit(id, routeState.routePathSegments); - } - - /** - * @inheritDoc - */ - idle(componentState: ComponentState): void { - this.store$.dispatch(ComponentIdle.of(componentState)); - } - - /** - * @inheritDoc - */ - load(componentState: ComponentState): Observable { - this.routerService - .get() - .pipe(take(1)) - .subscribe((routerState: RouterState) => { - if (componentState.status === INITIALIZED) { - this.store$.dispatch( - ComponentIdle.of( - componentState.copy({ - status: IDLE, - navigationId: routerState.navigationId - }) - ) - ); - } - - this.store$.dispatch( - ComponentLoading.of( - componentState.copy({ - status: LOADING, - navigationId: routerState.navigationId - }) - ) - ); - }); - - return this.onLoaded(componentState.id, componentState.routePathSegments); - } - - /** - * @inheritDoc - */ - update(componentState: ComponentState): void { - this.routerService - .get() - .pipe(take(1)) - .subscribe((routerState) => { - this.store$.dispatch( - ComponentUpdate.of( - componentState.copy({ - navigationId: routerState.navigationId - }) - ) - ); - }); - } - - /** - * @inheritDoc - */ - hasInSegment(id: string, routePathSegments: string[]): Observable { - return this.store$.select(STORE_COMPONENTS).pipe( - withLatestFrom(this.routerService.getState()), - map(([literalComponentsState, routeState]) => - this._isComponentInStatus(id, routePathSegments, literalComponentsState, routeState, ['*']) + private readonly componentsStateHelper: ComponentsStateHelper; + + /** + * ** Constructor. + */ + constructor( + private readonly store$: Store, + private readonly routerService: RouterService, + ) { + super(); + + this.componentsStateHelper = new ComponentsStateHelper(); + } + + /** + * @inheritDoc + */ + init(id: string, routeState: RouteState): Observable { + this.store$ + .select((store) => store) + .pipe( + take(1), + map((store) => + this._getComponentState( + id, + routeState.routePathSegments, + store.router, + store.components, + ), + ), + ) + .subscribe((componentState) => { + if (componentState.status === INITIALIZED) { + this.store$.dispatch(ComponentInit.of(componentState)); + } + }); + + return this.onInit(id, routeState.routePathSegments); + } + + /** + * @inheritDoc + */ + idle(componentState: ComponentState): void { + this.store$.dispatch(ComponentIdle.of(componentState)); + } + + /** + * @inheritDoc + */ + load(componentState: ComponentState): Observable { + this.routerService + .get() + .pipe(take(1)) + .subscribe((routerState: RouterState) => { + if (componentState.status === INITIALIZED) { + this.store$.dispatch( + ComponentIdle.of( + componentState.copy({ + status: IDLE, + navigationId: routerState.navigationId, + }), ), - take(1) - ); - } + ); + } - /** - * @inheritDoc - */ - onInit(id: string, routePathSegments: string[]): Observable { - return this.store$.select(STORE_COMPONENTS).pipe( - withLatestFrom(this.routerService.getState()), - map(([literalComponentsState, routeState]) => - this._isComponentInStatus(id, routePathSegments, literalComponentsState, routeState, ['*']) - ), - filter((isInitialized) => isInitialized), - switchMap(() => this.getModel(id, routePathSegments, ['*'])), - take(1) + this.store$.dispatch( + ComponentLoading.of( + componentState.copy({ + status: LOADING, + navigationId: routerState.navigationId, + }), + ), ); - } - - /** - * @inheritDoc - */ - onLoaded(id: string, routePathSegments: string[]): Observable { - return this.store$.select(STORE_COMPONENTS).pipe( - withLatestFrom(this.routerService.getState()), - map(([literalComponentsState, routeState]) => - this._isComponentInStatus(id, routePathSegments, literalComponentsState, routeState, [LOADED, FAILED]) - ), - filter((isLoaded) => isLoaded), - switchMap(() => this.getModel(id, routePathSegments)), - take(1) + }); + + return this.onLoaded(componentState.id, componentState.routePathSegments); + } + + /** + * @inheritDoc + */ + update(componentState: ComponentState): void { + this.routerService + .get() + .pipe(take(1)) + .subscribe((routerState) => { + this.store$.dispatch( + ComponentUpdate.of( + componentState.copy({ + navigationId: routerState.navigationId, + }), + ), ); + }); + } + + /** + * @inheritDoc + */ + hasInSegment(id: string, routePathSegments: string[]): Observable { + return this.store$.select(STORE_COMPONENTS).pipe( + withLatestFrom(this.routerService.getState()), + map(([literalComponentsState, routeState]) => + this._isComponentInStatus( + id, + routePathSegments, + literalComponentsState, + routeState, + ["*"], + ), + ), + take(1), + ); + } + + /** + * @inheritDoc + */ + onInit(id: string, routePathSegments: string[]): Observable { + return this.store$.select(STORE_COMPONENTS).pipe( + withLatestFrom(this.routerService.getState()), + map(([literalComponentsState, routeState]) => + this._isComponentInStatus( + id, + routePathSegments, + literalComponentsState, + routeState, + ["*"], + ), + ), + filter((isInitialized) => isInitialized), + switchMap(() => this.getModel(id, routePathSegments, ["*"])), + take(1), + ); + } + + /** + * @inheritDoc + */ + onLoaded( + id: string, + routePathSegments: string[], + ): Observable { + return this.store$.select(STORE_COMPONENTS).pipe( + withLatestFrom(this.routerService.getState()), + map(([literalComponentsState, routeState]) => + this._isComponentInStatus( + id, + routePathSegments, + literalComponentsState, + routeState, + [LOADED, FAILED], + ), + ), + filter((isLoaded) => isLoaded), + switchMap(() => this.getModel(id, routePathSegments)), + take(1), + ); + } + + /** + * @inheritDoc + */ + getModel( + id: string, + routePathSegments: string[], + statusWatch?: Array, + ): Observable { + const _statusWatch = statusWatch ?? [LOADED, FAILED]; + + return this.store$.select(STORE_COMPONENTS).pipe( + switchMap((literalComponentsState) => + this.routerService + .get() + .pipe(map((routerState) => [literalComponentsState, routerState])), + ), + filter( + ([literalComponentsState, routerState]: [ + LiteralComponentsState, + RouterState, + ]) => + this._isComponentInStatus( + id, + routePathSegments, + literalComponentsState, + routerState.state, + _statusWatch, + ), + ), + map(([literalComponentsState, routerState]) => + this._createModel( + id, + routePathSegments, + literalComponentsState, + routerState, + ), + ), + ); + } + + /** + * @inheritDoc + */ + dispatchAction( + type: string, + componentState: ComponentState, + task?: string, + ): void { + this.getModel(componentState.id, componentState.routePathSegments, ["*"]) + .pipe(take(1)) + .subscribe((model) => + this.store$.dispatch( + GenericAction.of(type, model.getComponentState(), task), + ), + ); + } + + /** + * @inheritDoc + */ + initialize() { + // No-op. + } + + // Get Component State from Store if exist, otherwise create new State. + private _getComponentState( + id: string, + routePathSegments: string[], + routerState: RouterState, + literalComponentsState: LiteralComponentsState, + ): ComponentState { + let _navigationId: number = null; + let _routePath: string = null; + let _routePathSegments: string[] = []; + + if (routerState) { + _navigationId = routerState.navigationId; + + if (routerState.state && !routePathSegments) { + _routePath = routerState.state.routePath; + _routePathSegments = routerState.state.routePathSegments; + } } - /** - * @inheritDoc - */ - getModel(id: string, routePathSegments: string[], statusWatch?: Array): Observable { - const _statusWatch = statusWatch ?? [LOADED, FAILED]; - - return this.store$.select(STORE_COMPONENTS).pipe( - switchMap((literalComponentsState) => - this.routerService.get().pipe(map((routerState) => [literalComponentsState, routerState])) - ), - filter(([literalComponentsState, routerState]: [LiteralComponentsState, RouterState]) => - this._isComponentInStatus(id, routePathSegments, literalComponentsState, routerState.state, _statusWatch) - ), - map(([literalComponentsState, routerState]) => this._createModel(id, routePathSegments, literalComponentsState, routerState)) - ); + if (routePathSegments) { + _routePath = routePathSegments.slice().pop(); + _routePathSegments = routePathSegments; } - /** - * @inheritDoc - */ - dispatchAction(type: string, componentState: ComponentState, task?: string): void { - this.getModel(componentState.id, componentState.routePathSegments, ['*']) - .pipe(take(1)) - .subscribe((model) => this.store$.dispatch(GenericAction.of(type, model.getComponentState(), task))); - } + let componentState = this.componentsStateHelper + .setState(literalComponentsState) + .getComponentState(id, _routePathSegments); - /** - * @inheritDoc - */ - initialize() { - // No-op. + if (componentState) { + return componentState; } - // Get Component State from Store if exist, otherwise create new State. - private _getComponentState( - id: string, - routePathSegments: string[], - routerState: RouterState, - literalComponentsState: LiteralComponentsState - ): ComponentState { - let _navigationId: number = null; - let _routePath: string = null; - let _routePathSegments: string[] = []; - - if (routerState) { - _navigationId = routerState.navigationId; - - if (routerState.state && !routePathSegments) { - _routePath = routerState.state.routePath; - _routePathSegments = routerState.state.routePathSegments; - } - } - - if (routePathSegments) { - _routePath = routePathSegments.slice().pop(); - _routePathSegments = routePathSegments; - } - - let componentState = this.componentsStateHelper.setState(literalComponentsState).getComponentState(id, _routePathSegments); - - if (componentState) { - return componentState; - } - - componentState = ComponentStateImpl.of({ - id, - status: INITIALIZED, - routePath: _routePath, - routePathSegments: _routePathSegments, - navigationId: _navigationId - }); - - return componentState; + componentState = ComponentStateImpl.of({ + id, + status: INITIALIZED, + routePath: _routePath, + routePathSegments: _routePathSegments, + navigationId: _navigationId, + }); + + return componentState; + } + + // Utility method that filter if provided state is in desired status. + private _isComponentInStatus( + id: string, + routePathSegments: string[], + literalComponentsState: LiteralComponentsState, + routeState: RouteState, + statusWatch: Array, + ): boolean { + let _routePathSegments: string[] = []; + + if (CollectionsUtil.isArray(routePathSegments)) { + _routePathSegments = routePathSegments; + } else if (routeState) { + _routePathSegments = routeState.routePathSegments; } - // Utility method that filter if provided state is in desired status. - private _isComponentInStatus( - id: string, - routePathSegments: string[], - literalComponentsState: LiteralComponentsState, - routeState: RouteState, - statusWatch: Array - ): boolean { - let _routePathSegments: string[] = []; - - if (CollectionsUtil.isArray(routePathSegments)) { - _routePathSegments = routePathSegments; - } else if (routeState) { - _routePathSegments = routeState.routePathSegments; - } - - const componentLiteralState = this.componentsStateHelper - .setState(literalComponentsState) - .getLiteralComponentState(id, _routePathSegments); - - if (!componentLiteralState) { - return false; - } + const componentLiteralState = this.componentsStateHelper + .setState(literalComponentsState) + .getLiteralComponentState(id, _routePathSegments); - if (statusWatch.indexOf('*') !== -1) { - return true; - } + if (!componentLiteralState) { + return false; + } - return statusWatch.indexOf(componentLiteralState.status) !== -1; + if (statusWatch.indexOf("*") !== -1) { + return true; } - // Creates Model from provided data. - private _createModel( - id: string, - routePathSegments: string[], - literalComponentsState: LiteralComponentsState, - routerState: RouterState - ): ComponentModel { - let _routePathSegments: string[] = []; - - if (CollectionsUtil.isArray(routePathSegments)) { - _routePathSegments = routePathSegments; - } else if (routerState && routerState.state) { - _routePathSegments = routerState.state.routePathSegments; - } + return statusWatch.indexOf(componentLiteralState.status) !== -1; + } + + // Creates Model from provided data. + private _createModel( + id: string, + routePathSegments: string[], + literalComponentsState: LiteralComponentsState, + routerState: RouterState, + ): ComponentModel { + let _routePathSegments: string[] = []; + + if (CollectionsUtil.isArray(routePathSegments)) { + _routePathSegments = routePathSegments; + } else if (routerState && routerState.state) { + _routePathSegments = routerState.state.routePathSegments; + } - const componentState = this.componentsStateHelper.setState(literalComponentsState).getComponentState(id, _routePathSegments); + const componentState = this.componentsStateHelper + .setState(literalComponentsState) + .getComponentState(id, _routePathSegments); - return ComponentModel.of(componentState, CollectionsUtil.cloneDeep(routerState)); - } + return ComponentModel.of( + componentState, + CollectionsUtil.cloneDeep(routerState), + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts index 12829cf228..974413ba81 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component.service'; +export * from "./component.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts index 5d3dc7bd9e..4137684ee3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts @@ -4,288 +4,298 @@ */ import { - COMPONENT_CLEAR_DATA, - COMPONENT_FAILED, - COMPONENT_IDLE, - COMPONENT_INIT, - COMPONENT_LOADED, - COMPONENT_LOADING, - COMPONENT_UPDATE, - ComponentClearData, - ComponentFailed, - ComponentIdle, - ComponentInit, - ComponentLoaded, - ComponentLoading, - ComponentUpdate -} from './component.actions'; -import { ComponentStateImpl } from '../../model'; -import { BaseAction, BaseActionWithPayload } from '../../../ngrx'; - -describe('ComponentActions', () => { - describe('ComponentInit', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentInit(ComponentStateImpl.of({}))).toBeDefined(); - }); + COMPONENT_CLEAR_DATA, + COMPONENT_FAILED, + COMPONENT_IDLE, + COMPONENT_INIT, + COMPONENT_LOADED, + COMPONENT_LOADING, + COMPONENT_UPDATE, + ComponentClearData, + ComponentFailed, + ComponentIdle, + ComponentInit, + ComponentLoaded, + ComponentLoading, + ComponentUpdate, +} from "./component.actions"; +import { ComponentStateImpl } from "../../model"; +import { BaseAction, BaseActionWithPayload } from "../../../ngrx"; + +describe("ComponentActions", () => { + describe("ComponentInit", () => { + it("should verify instance is created", () => { + // Then + expect(() => new ComponentInit(ComponentStateImpl.of({}))).toBeDefined(); + }); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentInit(ComponentStateImpl.of({})); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentInit(ComponentStateImpl.of({})); - // Then - expect(instance.type).toEqual(COMPONENT_INIT); - }); + // Then + expect(instance.type).toEqual(COMPONENT_INIT); + }); + + it("should verify prototype chaining", () => { + // When + const instance = new ComponentInit(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentInit(ComponentStateImpl.of({})); + const instance = ComponentInit.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); + expect(instance).toBeInstanceOf(ComponentInit); + }); }); + }); + }); + }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentInit.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentInit); - }); - }); - }); - }); + describe("ComponentIdle", () => { + it("should verify instance is created", () => { + // Then + expect(() => new ComponentIdle(ComponentStateImpl.of({}))).toBeDefined(); }); - describe('ComponentIdle', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentIdle(ComponentStateImpl.of({}))).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentIdle(ComponentStateImpl.of({})); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentIdle(ComponentStateImpl.of({})); + // Then + expect(instance.type).toEqual(COMPONENT_IDLE); + }); - // Then - expect(instance.type).toEqual(COMPONENT_IDLE); - }); + it("should verify prototype chaining", () => { + // When + const instance = new ComponentIdle(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentIdle(ComponentStateImpl.of({})); + const instance = ComponentIdle.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentIdle.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentIdle); - }); - }); - }); + expect(instance).toBeInstanceOf(ComponentIdle); + }); }); + }); + }); + }); + + describe("ComponentLoading", () => { + it("should verify instance is created", () => { + // Then + expect( + () => new ComponentLoading(ComponentStateImpl.of({})), + ).toBeDefined(); }); - describe('ComponentLoading', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentLoading(ComponentStateImpl.of({}))).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentLoading(ComponentStateImpl.of({})); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentLoading(ComponentStateImpl.of({})); + // Then + expect(instance.type).toEqual(COMPONENT_LOADING); + }); - // Then - expect(instance.type).toEqual(COMPONENT_LOADING); - }); + it("should verify prototype chaining", () => { + // When + const instance = new ComponentLoading(ComponentStateImpl.of({})); - it('should verify prototype chaining', () => { + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentLoading(ComponentStateImpl.of({})); + const instance = ComponentLoading.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentLoading.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentLoading); - }); - }); - }); + expect(instance).toBeInstanceOf(ComponentLoading); + }); }); + }); + }); + }); + + describe("ComponentLoaded", () => { + it("should verify instance is created", () => { + // Then + expect( + () => new ComponentLoaded(ComponentStateImpl.of({})), + ).toBeDefined(); }); - describe('ComponentLoaded', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentLoaded(ComponentStateImpl.of({}))).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentLoaded(ComponentStateImpl.of({})); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentLoaded(ComponentStateImpl.of({})); + // Then + expect(instance.type).toEqual(COMPONENT_LOADED); + }); - // Then - expect(instance.type).toEqual(COMPONENT_LOADED); - }); + it("should verify prototype chaining", () => { + // When + const instance = new ComponentLoaded(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentLoaded(ComponentStateImpl.of({})); + const instance = ComponentLoaded.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentLoaded.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentLoaded); - }); - }); - }); + expect(instance).toBeInstanceOf(ComponentLoaded); + }); }); + }); + }); + }); + + describe("ComponentFailed", () => { + it("should verify instance is created", () => { + // Then + expect( + () => new ComponentFailed(ComponentStateImpl.of({})), + ).toBeDefined(); }); - describe('ComponentFailed', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentFailed(ComponentStateImpl.of({}))).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentFailed(ComponentStateImpl.of({})); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentFailed(ComponentStateImpl.of({})); + // Then + expect(instance.type).toEqual(COMPONENT_FAILED); + }); - // Then - expect(instance.type).toEqual(COMPONENT_FAILED); - }); + it("should verify prototype chaining", () => { + // When + const instance = new ComponentFailed(ComponentStateImpl.of({})); - it('should verify prototype chaining', () => { + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentFailed(ComponentStateImpl.of({})); + const instance = ComponentFailed.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentFailed.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentFailed); - }); - }); - }); + expect(instance).toBeInstanceOf(ComponentFailed); + }); }); + }); + }); + }); + + describe("ComponentUpdate", () => { + it("should verify instance is created", () => { + // Then + expect( + () => new ComponentUpdate(ComponentStateImpl.of({})), + ).toBeDefined(); }); - describe('ComponentUpdate', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentUpdate(ComponentStateImpl.of({}))).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentUpdate(ComponentStateImpl.of({})); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentUpdate(ComponentStateImpl.of({})); + // Then + expect(instance.type).toEqual(COMPONENT_UPDATE); + }); - // Then - expect(instance.type).toEqual(COMPONENT_UPDATE); - }); + it("should verify prototype chaining", () => { + // When + const instance = new ComponentUpdate(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentUpdate(ComponentStateImpl.of({})); + const instance = ComponentUpdate.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentUpdate.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentUpdate); - }); - }); - }); + expect(instance).toBeInstanceOf(ComponentUpdate); + }); }); + }); + }); + }); + + describe("ComponentClearData", () => { + it("should verify instance is created", () => { + // Then + expect( + () => new ComponentClearData(ComponentStateImpl.of({})), + ).toBeDefined(); }); - describe('ComponentClearData', () => { - it('should verify instance is created', () => { - // Then - expect(() => new ComponentClearData(ComponentStateImpl.of({}))).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new ComponentClearData(ComponentStateImpl.of({})); - it('should verify correct type is assigned', () => { - // When - const instance = new ComponentClearData(ComponentStateImpl.of({})); + // Then + expect(instance.type).toEqual(COMPONENT_CLEAR_DATA); + }); - // Then - expect(instance.type).toEqual(COMPONENT_CLEAR_DATA); - }); + it("should verify prototype chaining", () => { + // When + const instance = new ComponentClearData(ComponentStateImpl.of({})); - it('should verify prototype chaining', () => { + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new ComponentClearData(ComponentStateImpl.of({})); + const instance = ComponentClearData.of(ComponentStateImpl.of({})); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ComponentClearData.of(ComponentStateImpl.of({})); - - // Then - expect(instance).toBeInstanceOf(ComponentClearData); - }); - }); - }); + expect(instance).toBeInstanceOf(ComponentClearData); + }); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts index 8228da7d95..830f531bf2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts @@ -3,165 +3,165 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { BaseActionWithPayload } from '../../../ngrx/actions'; +import { BaseActionWithPayload } from "../../../ngrx/actions"; -import { ComponentState } from '../../model'; +import { ComponentState } from "../../model"; /** * ** Action Identifier for Component Initialization. */ -export const COMPONENT_INIT = '[component] Init'; +export const COMPONENT_INIT = "[component] Init"; /** * ** Action Identifier for Component Idle. */ -export const COMPONENT_IDLE = '[component] Idle'; +export const COMPONENT_IDLE = "[component] Idle"; /** * ** Action Identifier for Component start Loading data. */ -export const COMPONENT_LOADING = '[component] Loading'; +export const COMPONENT_LOADING = "[component] Loading"; /** * ** Action Identifier for Component Loaded data. */ -export const COMPONENT_LOADED = '[component] Loaded'; +export const COMPONENT_LOADED = "[component] Loaded"; /** * ** Action Identifier for Component Failed loading data. */ -export const COMPONENT_FAILED = '[component] Failed'; +export const COMPONENT_FAILED = "[component] Failed"; /** * ** Action Identifier for Component Update state. */ -export const COMPONENT_UPDATE = '[component] Update'; +export const COMPONENT_UPDATE = "[component] Update"; /** * ** Action Identifier for Component Clear data. */ -export const COMPONENT_CLEAR_DATA = '[component] Clear data'; +export const COMPONENT_CLEAR_DATA = "[component] Clear data"; /** * ** Action for Component Initialization. */ export class ComponentInit extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_INIT, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentInit(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_INIT, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentInit(payload); + } } /** * ** Action for Component Idle. */ export class ComponentIdle extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_IDLE, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentIdle(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_IDLE, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentIdle(payload); + } } /** * ** Action for Component Loading. */ export class ComponentLoading extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_LOADING, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentLoading(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_LOADING, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentLoading(payload); + } } /** * ** Action for Component Loaded. */ export class ComponentLoaded extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_LOADED, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentLoaded(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_LOADED, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentLoaded(payload); + } } /** * ** Action for Component Failed. */ export class ComponentFailed extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_FAILED, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentFailed(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_FAILED, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentFailed(payload); + } } /** * ** Action for Component Update. */ export class ComponentUpdate extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_UPDATE, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentUpdate(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_UPDATE, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentUpdate(payload); + } } /** * ** Action for Component Clear Data. */ export class ComponentClearData extends BaseActionWithPayload { - constructor(payload: ComponentState) { - super(COMPONENT_CLEAR_DATA, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: ComponentState) { - return new ComponentClearData(payload); - } + constructor(payload: ComponentState) { + super(COMPONENT_CLEAR_DATA, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentClearData(payload); + } } /** * ** Union of all Actions that could be use as a type in Effects/Reducers/etc... */ export type ComponentActions = - | ComponentInit - | ComponentIdle - | ComponentLoading - | ComponentLoaded - | ComponentFailed - | ComponentUpdate - | ComponentClearData; + | ComponentInit + | ComponentIdle + | ComponentLoading + | ComponentLoaded + | ComponentFailed + | ComponentUpdate + | ComponentClearData; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts index 0a44c2f465..3b2719952e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component.actions'; +export * from "./component.actions"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts index 387ff0e124..7288f1c5e5 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './actions'; -export * from './reducers'; -export * from './operators'; +export * from "./actions"; +export * from "./reducers"; +export * from "./operators"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts index 6dac23d519..364dc4bf03 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts @@ -3,218 +3,278 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { throwError } from 'rxjs'; - -import { marbles } from 'rxjs-marbles/jasmine'; - -import { CollectionsUtil } from '../../../../utils'; - -import { generateErrorCodes } from '../../../../unit-testing'; - -import { ErrorStoreImpl } from '../../../error'; - -import { RouterState, RouteSegments, RouteState } from '../../../router'; - -import { BaseActionWithPayload } from '../../../ngrx'; - -import { ComponentModel, ComponentState, ComponentStateImpl, FAILED, INITIALIZED, LOADED } from '../../model'; -import { ComponentService } from '../../services'; - -import { ComponentFailed } from '../actions'; - -import { getModel, getModelAndTask, handleActionError } from './component-rx.operators'; - -describe('rxjs operators', () => { - describe('|getModel|', () => { - it( - 'should verify will create ComponentModel down the stream', - marbles((m) => { - // Given - const inputActionStream$ = m.cold('----a---b----', { - a: new ActionStub( - ComponentStateImpl.of({ - id: 'test1', - status: INITIALIZED, - routePathSegments: ['entity/10'] - }) - ), - b: new ActionStub( - ComponentStateImpl.of({ - id: 'test2', - status: LOADED, - routePathSegments: ['entity/225'] - }) - ) - }); - const getModelStream1$ = m.cold('-a', { - a: ComponentModel.of( - ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/10'), 'entity/5'), 3) - ) - }); - const getModelStream2$ = m.cold('--a', { - a: ComponentModel.of( - ComponentStateImpl.of({ id: 'test5', status: LOADED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/225'), 'entity/225'), 3) - ) - }); - const expectedOutputStream$ = m.cold('-----a----b----', { - a: ComponentModel.of( - ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/10'), 'entity/5'), 3) - ), - b: ComponentModel.of( - ComponentStateImpl.of({ id: 'test5', status: LOADED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/225'), 'entity/225'), 3) - ) - }); - - const componentServiceStub = jasmine.createSpyObj('componentService', ['getModel']); - let cnt = 0; - componentServiceStub.getModel.and.callFake(() => { - cnt++; - if (cnt === 1) { - return getModelStream1$; - } else { - return getModelStream2$; - } - }); - - // When - const response$ = getModel(componentServiceStub)(inputActionStream$); - - // Then - m.expect(response$).toBeObservable(expectedOutputStream$); - }) +import { throwError } from "rxjs"; + +import { marbles } from "rxjs-marbles/jasmine"; + +import { CollectionsUtil } from "../../../../utils"; + +import { generateErrorCodes } from "../../../../unit-testing"; + +import { ErrorStoreImpl } from "../../../error"; + +import { RouterState, RouteSegments, RouteState } from "../../../router"; + +import { BaseActionWithPayload } from "../../../ngrx"; + +import { + ComponentModel, + ComponentState, + ComponentStateImpl, + FAILED, + INITIALIZED, + LOADED, +} from "../../model"; +import { ComponentService } from "../../services"; + +import { ComponentFailed } from "../actions"; + +import { + getModel, + getModelAndTask, + handleActionError, +} from "./component-rx.operators"; + +describe("rxjs operators", () => { + describe("|getModel|", () => { + it( + "should verify will create ComponentModel down the stream", + marbles((m) => { + // Given + const inputActionStream$ = m.cold("----a---b----", { + a: new ActionStub( + ComponentStateImpl.of({ + id: "test1", + status: INITIALIZED, + routePathSegments: ["entity/10"], + }), + ), + b: new ActionStub( + ComponentStateImpl.of({ + id: "test2", + status: LOADED, + routePathSegments: ["entity/225"], + }), + ), + }); + const getModelStream1$ = m.cold("-a", { + a: ComponentModel.of( + ComponentStateImpl.of({ id: "test1", status: INITIALIZED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/10"), "entity/5"), + 3, + ), + ), + }); + const getModelStream2$ = m.cold("--a", { + a: ComponentModel.of( + ComponentStateImpl.of({ id: "test5", status: LOADED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/225"), "entity/225"), + 3, + ), + ), + }); + const expectedOutputStream$ = m.cold("-----a----b----", { + a: ComponentModel.of( + ComponentStateImpl.of({ id: "test1", status: INITIALIZED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/10"), "entity/5"), + 3, + ), + ), + b: ComponentModel.of( + ComponentStateImpl.of({ id: "test5", status: LOADED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/225"), "entity/225"), + 3, + ), + ), + }); + + const componentServiceStub = jasmine.createSpyObj( + "componentService", + ["getModel"], + ); + let cnt = 0; + componentServiceStub.getModel.and.callFake(() => { + cnt++; + if (cnt === 1) { + return getModelStream1$; + } else { + return getModelStream2$; + } + }); + + // When + const response$ = getModel(componentServiceStub)(inputActionStream$); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + }), + ); + }); + + describe("|getModelAndTask|", () => { + it( + "should verify will create ComponentModel down the stream", + marbles((m) => { + // Given + const taskIdentifier1 = `patch_entity __ ${new Date().toISOString()}`; + const taskIdentifier2 = `patch_dataset __ ${new Date().toISOString()}`; + spyOn(CollectionsUtil, "interpolateString").and.returnValues( + taskIdentifier1, + taskIdentifier2, ); - }); - - describe('|getModelAndTask|', () => { - it( - 'should verify will create ComponentModel down the stream', - marbles((m) => { - // Given - const taskIdentifier1 = `patch_entity __ ${new Date().toISOString()}`; - const taskIdentifier2 = `patch_dataset __ ${new Date().toISOString()}`; - spyOn(CollectionsUtil, 'interpolateString').and.returnValues(taskIdentifier1, taskIdentifier2); - const inputActionStream$ = m.cold('----a---b----', { - a: new ActionStub( - ComponentStateImpl.of({ - id: 'test1', - status: INITIALIZED, - routePathSegments: ['entity/10'] - }), - taskIdentifier1 - ), - b: new ActionStub( - ComponentStateImpl.of({ - id: 'test2', - status: LOADED, - routePathSegments: ['entity/225'] - }), - taskIdentifier2 - ) - }); - const getModelStream1$ = m.cold('-a', { - a: ComponentModel.of( - ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/10'), 'entity/5'), 3) - ) - }); - const getModelStream2$ = m.cold('--a', { - a: ComponentModel.of( - ComponentStateImpl.of({ id: 'test5', status: LOADED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/225'), 'entity/225'), 3) - ) - }); - const expectedOutputStream$ = m.cold('-----a----b----', { - a: [ - ComponentModel.of( - ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/10'), 'entity/5'), 3) - ), - taskIdentifier1 - ] as [ComponentModel, string], - b: [ - ComponentModel.of( - ComponentStateImpl.of({ id: 'test5', status: LOADED }), - RouterState.of(RouteState.of(RouteSegments.of('entity/225'), 'entity/225'), 3) - ), - taskIdentifier2 - ] as [ComponentModel, string] - }); - - const componentServiceStub = jasmine.createSpyObj('componentService', ['getModel']); - let cnt = 0; - componentServiceStub.getModel.and.callFake(() => { - cnt++; - if (cnt === 1) { - return getModelStream1$; - } else { - return getModelStream2$; - } - }); - - // When - const response$ = getModelAndTask(componentServiceStub)(inputActionStream$); - - // Then - m.expect(response$).toBeObservable(expectedOutputStream$); - }) + const inputActionStream$ = m.cold("----a---b----", { + a: new ActionStub( + ComponentStateImpl.of({ + id: "test1", + status: INITIALIZED, + routePathSegments: ["entity/10"], + }), + taskIdentifier1, + ), + b: new ActionStub( + ComponentStateImpl.of({ + id: "test2", + status: LOADED, + routePathSegments: ["entity/225"], + }), + taskIdentifier2, + ), + }); + const getModelStream1$ = m.cold("-a", { + a: ComponentModel.of( + ComponentStateImpl.of({ id: "test1", status: INITIALIZED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/10"), "entity/5"), + 3, + ), + ), + }); + const getModelStream2$ = m.cold("--a", { + a: ComponentModel.of( + ComponentStateImpl.of({ id: "test5", status: LOADED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/225"), "entity/225"), + 3, + ), + ), + }); + const expectedOutputStream$ = m.cold("-----a----b----", { + a: [ + ComponentModel.of( + ComponentStateImpl.of({ id: "test1", status: INITIALIZED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/10"), "entity/5"), + 3, + ), + ), + taskIdentifier1, + ] as [ComponentModel, string], + b: [ + ComponentModel.of( + ComponentStateImpl.of({ id: "test5", status: LOADED }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/225"), "entity/225"), + 3, + ), + ), + taskIdentifier2, + ] as [ComponentModel, string], + }); + + const componentServiceStub = jasmine.createSpyObj( + "componentService", + ["getModel"], + ); + let cnt = 0; + componentServiceStub.getModel.and.callFake(() => { + cnt++; + if (cnt === 1) { + return getModelStream1$; + } else { + return getModelStream2$; + } + }); + + // When + const response$ = + getModelAndTask(componentServiceStub)(inputActionStream$); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + }), + ); + }); + + describe("|handleActionError|", () => { + it( + "should verify will handle Stream error and return ComponentFailed action", + marbles((m) => { + // Given + const inputModel = ComponentModel.of( + ComponentStateImpl.of({ + id: "test1", + status: INITIALIZED, + navigationId: 3, + }), + RouterState.of( + RouteState.of(RouteSegments.of("entity/10"), "entity/5"), + 3, + ), ); - }); - - describe('|handleActionError|', () => { - it( - 'should verify will handle Stream error and return ComponentFailed action', - marbles((m) => { - // Given - const inputModel = ComponentModel.of( - ComponentStateImpl.of({ id: 'test1', status: INITIALIZED, navigationId: 3 }), - RouterState.of(RouteState.of(RouteSegments.of('entity/10'), 'entity/5'), 3) - ); - - const dateNow = Date.now(); - spyOn(CollectionsUtil, 'dateNow').and.returnValue(dateNow); - - const error = new Error('Http Failure'); - const getResponseStream$ = throwError(() => error); - const serviceStub = jasmine.createSpyObj<{ load: () => void }>('serviceStub', ['load']); - const className = CollectionsUtil.generateRandomString(); - const taskName = CollectionsUtil.generateRandomString(); - const objectUUID = CollectionsUtil.generateObjectUUID(className); - const errorCodes = generateErrorCodes(serviceStub, ['load'], className); - const expectedOutputStream$ = m.cold('(a|)', { - a: ComponentFailed.of( - ComponentStateImpl.of({ - id: 'test1', - status: FAILED, - navigationId: 3, - task: taskName, - errors: ErrorStoreImpl.of([ - { - error, - code: errorCodes.load.Unknown, - objectUUID, - time: dateNow - } - ]) - }) - ) - }); - - // When - const response$ = handleActionError(() => [inputModel, taskName, objectUUID, errorCodes.load])(getResponseStream$); - - // Then - m.expect(response$).toBeObservable(expectedOutputStream$); - }) + + const dateNow = Date.now(); + spyOn(CollectionsUtil, "dateNow").and.returnValue(dateNow); + + const error = new Error("Http Failure"); + const getResponseStream$ = throwError(() => error); + const serviceStub = jasmine.createSpyObj<{ load: () => void }>( + "serviceStub", + ["load"], ); - }); + const className = CollectionsUtil.generateRandomString(); + const taskName = CollectionsUtil.generateRandomString(); + const objectUUID = CollectionsUtil.generateObjectUUID(className); + const errorCodes = generateErrorCodes(serviceStub, ["load"], className); + const expectedOutputStream$ = m.cold("(a|)", { + a: ComponentFailed.of( + ComponentStateImpl.of({ + id: "test1", + status: FAILED, + navigationId: 3, + task: taskName, + errors: ErrorStoreImpl.of([ + { + error, + code: errorCodes.load.Unknown, + objectUUID, + time: dateNow, + }, + ]), + }), + ), + }); + + // When + const response$ = handleActionError(() => [ + inputModel, + taskName, + objectUUID, + errorCodes.load, + ])(getResponseStream$); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + }), + ); + }); }); class ActionStub extends BaseActionWithPayload { - constructor(payload: ComponentState, task?: string) { - super('[component] action stub', payload, task); - } + constructor(payload: ComponentState, task?: string) { + super("[component] action stub", payload, task); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts index 9d614d10f3..36eed97198 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts @@ -3,19 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { iif, Observable, of, throwError } from 'rxjs'; -import { catchError, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { iif, Observable, of, throwError } from "rxjs"; +import { catchError, map, mergeMap, switchMap, take } from "rxjs/operators"; -import { ServiceHttpErrorCodes } from '../../../../common'; +import { ServiceHttpErrorCodes } from "../../../../common"; -import { processServiceRequestError } from '../../../error'; +import { processServiceRequestError } from "../../../error"; -import { BaseActionWithPayload } from '../../../ngrx'; +import { BaseActionWithPayload } from "../../../ngrx"; -import { ComponentService } from '../../services'; -import { ComponentModel, ComponentState } from '../../model'; +import { ComponentService } from "../../services"; +import { ComponentModel, ComponentState } from "../../model"; -import { ComponentFailed } from '../actions'; +import { ComponentFailed } from "../actions"; /** * ** RXJS Operator that fetch Component Model from Store using provided ComponentService. @@ -23,12 +23,14 @@ import { ComponentFailed } from '../actions'; * - Returns Observable */ export const getModel = - (componentService: ComponentService) => - (source$: Observable>): Observable => - source$.pipe( - extractAction, - switchMap((action) => subscribeForModel(action, componentService)) - ); + (componentService: ComponentService) => + ( + source$: Observable>, + ): Observable => + source$.pipe( + extractAction, + switchMap((action) => subscribeForModel(action, componentService)), + ); /** * ** RXJS Operator that fetch Component Model from Store using provided ComponentService and consume task from Action. @@ -36,14 +38,18 @@ export const getModel = * - Returns Observable<[ComponentModel, string]> */ export const getModelAndTask = - (componentService: ComponentService) => - (source$: Observable>): Observable<[ComponentModel, string]> => - source$.pipe( - extractAction, - switchMap((action) => - subscribeForModel(action, componentService).pipe(map((model) => [model, action.task] as [ComponentModel, string])) - ) - ); + (componentService: ComponentService) => + ( + source$: Observable>, + ): Observable<[ComponentModel, string]> => + source$.pipe( + extractAction, + switchMap((action) => + subscribeForModel(action, componentService).pipe( + map((model) => [model, action.task] as [ComponentModel, string]), + ), + ), + ); export type TaskID = string; export type ObjectUUID = string; @@ -61,38 +67,69 @@ export type ObjectUUID = string; * - Factory callbacks should return tuple of [Component Model, Task Id, Object UUID, Error records codes from API service] */ export const handleActionError = - (paramFactory: () => [ComponentModel, TaskID, ObjectUUID, Record]) => - (source$: Observable>): Observable> => - source$.pipe( - catchError((error: unknown) => { - const [model, task, objectUUID, serviceHttpErrorCodes] = paramFactory(); + ( + paramFactory: () => [ + ComponentModel, + TaskID, + ObjectUUID, + Record, + ], + ) => + ( + source$: Observable>, + ): Observable> => + source$.pipe( + catchError((error: unknown) => { + const [model, task, objectUUID, serviceHttpErrorCodes] = paramFactory(); - try { - model.withError(processServiceRequestError(objectUUID, serviceHttpErrorCodes, error)); - } catch (e) { - console.error('handleActionError: Cannot process error', e); - } + try { + model.withError( + processServiceRequestError( + objectUUID, + serviceHttpErrorCodes, + error, + ), + ); + } catch (e) { + console.error("handleActionError: Cannot process error", e); + } - return of(ComponentFailed.of(model.withTask(task).withStatusFailed().getComponentState())); - }) + return of( + ComponentFailed.of( + model.withTask(task).withStatusFailed().getComponentState(), + ), ); + }), + ); /** * ** Validate if Action is instance of {@link BaseActionWithPayload} and pass down the stream. */ -const extractAction = (source$: Observable>) => - source$.pipe( - mergeMap((action) => - iif( - () => action instanceof BaseActionWithPayload, - of(action), - throwError(() => new Error('Unsupported Action type. It should be subclass of BaseActionWithPayload')) - ) - ) - ); +const extractAction = ( + source$: Observable>, +) => + source$.pipe( + mergeMap((action) => + iif( + () => action instanceof BaseActionWithPayload, + of(action), + throwError( + () => + new Error( + "Unsupported Action type. It should be subclass of BaseActionWithPayload", + ), + ), + ), + ), + ); /** * ** Make actual subscription to Store for ComponentModel. */ -const subscribeForModel = (action: BaseActionWithPayload, componentService: ComponentService) => - componentService.getModel(action.payload.id, action.payload.routePathSegments, ['*']).pipe(take(1)); +const subscribeForModel = ( + action: BaseActionWithPayload, + componentService: ComponentService, +) => + componentService + .getModel(action.payload.id, action.payload.routePathSegments, ["*"]) + .pipe(take(1)); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts index 84c8bea61b..de6c9009e2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component-rx.operators'; +export * from "./component-rx.operators"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts index f65c46b138..1b5eedd4da 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts @@ -5,309 +5,485 @@ /* eslint-disable arrow-body-style,prefer-arrow/prefer-arrow-functions */ -import { ROUTER_NAVIGATION, RouterNavigationPayload } from '@ngrx/router-store'; +import { ROUTER_NAVIGATION, RouterNavigationPayload } from "@ngrx/router-store"; -import { CallFake } from '../../../../unit-testing'; +import { CallFake } from "../../../../unit-testing"; -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { BaseActionWithPayload } from '../../../ngrx'; +import { BaseActionWithPayload } from "../../../ngrx"; -import { RouteSegments, RouteState } from '../../../router'; +import { RouteSegments, RouteState } from "../../../router"; import { - ComponentsStateHelper, - ComponentState, - FAILED, - IDLE, - INITIALIZED, - LiteralComponentsState, - LiteralComponentState, - LOADED, - LOADING, - StatusType -} from '../../model'; + ComponentsStateHelper, + ComponentState, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentsState, + LiteralComponentState, + LOADED, + LOADING, + StatusType, +} from "../../model"; import { - COMPONENT_CLEAR_DATA, - COMPONENT_FAILED, - COMPONENT_INIT, - COMPONENT_LOADING, - COMPONENT_UPDATE, - ComponentIdle, - ComponentLoaded -} from '../actions'; - -import { componentReducer } from './component.reducer'; - -describe('componentReducer', () => { - let id: string; - let routePathSegments: string[]; - let literalComponentsState: LiteralComponentsState; - let updateComponentHelperSpy: jasmine.Spy; - let getComponentHelperSpy: jasmine.Spy; - let getStateHelperSpy: jasmine.Spy; - - beforeEach(() => { - id = 'testComponent1'; - routePathSegments = ['test_entity/15']; - literalComponentsState = { - components: {}, - routePathSegments: {} - }; - updateComponentHelperSpy = spyOn(ComponentsStateHelper.prototype, 'updateLiteralComponentState').and.callFake(CallFake); - getComponentHelperSpy = spyOn(ComponentsStateHelper.prototype, 'getLiteralComponentState'); - getStateHelperSpy = spyOn(ComponentsStateHelper.prototype, 'getState'); + COMPONENT_CLEAR_DATA, + COMPONENT_FAILED, + COMPONENT_INIT, + COMPONENT_LOADING, + COMPONENT_UPDATE, + ComponentIdle, + ComponentLoaded, +} from "../actions"; + +import { componentReducer } from "./component.reducer"; + +describe("componentReducer", () => { + let id: string; + let routePathSegments: string[]; + let literalComponentsState: LiteralComponentsState; + let updateComponentHelperSpy: jasmine.Spy; + let getComponentHelperSpy: jasmine.Spy; + let getStateHelperSpy: jasmine.Spy; + + beforeEach(() => { + id = "testComponent1"; + routePathSegments = ["test_entity/15"]; + literalComponentsState = { + components: {}, + routePathSegments: {}, + }; + updateComponentHelperSpy = spyOn( + ComponentsStateHelper.prototype, + "updateLiteralComponentState", + ).and.callFake(CallFake); + getComponentHelperSpy = spyOn( + ComponentsStateHelper.prototype, + "getLiteralComponentState", + ); + getStateHelperSpy = spyOn(ComponentsStateHelper.prototype, "getState"); + }); + + describe("Actions::", () => { + describe("|COMPONENT_INIT|", () => { + it("should verify invokes correct methods", () => { + // Given + const action = new ActionStub( + COMPONENT_INIT, + createComponentState(null), + ); + getStateHelperSpy.and.returnValue( + createComponentsState(createComponentState(INITIALIZED).toLiteral()), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(result).toEqual( + createComponentsState(createComponentState(INITIALIZED).toLiteral()), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(INITIALIZED).toLiteral(), + ); + }); + }); + + describe("|COMPONENT_IDLE|", () => { + it("should verify invokes correct methods", () => { + // Given + const action = ComponentIdle.of( + createComponentState(INITIALIZED, "test-team", 4), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(INITIALIZED).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(IDLE, "test-team", 4).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(IDLE, "test-team", 4).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(IDLE, "test-team", 4).toLiteral(), + ); + }); + }); + + describe("|COMPONENT_LOADING|", () => { + it("should verify invokes correct methods", () => { + // Given + const action = new ActionStub( + COMPONENT_LOADING, + createComponentState(IDLE, "test-team", 4), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(IDLE).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(LOADING, "test-team", 4).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(LOADING, "test-team", 4).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(LOADING, "test-team", 4).toLiteral(), + ); + }); + }); + + describe("|COMPONENT_UPDATE|", () => { + it("should verify invokes correct methods (data from action)", () => { + // Given + const data = new Map([ + ["users", []], + ["cities", {}], + ]); + const action = new ActionStub( + COMPONENT_UPDATE, + createComponentState(IDLE, "test-team", 7, data), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(LOADING).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(IDLE, "test-team", 7, data).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(IDLE, "test-team", 7, data).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(IDLE, "test-team", 7, data).toLiteral(), + ); + }); + + it("should verify invokes correct methods (data from store)", () => { + // Given + const data = new Map([ + ["countries", []], + ["payload", {}], + ]); + const action = new ActionStub( + COMPONENT_UPDATE, + createComponentState(FAILED, "test-team", 6), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(LOADING, "test-team", 4, data).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(FAILED, "test-team", 6, data).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(FAILED, "test-team", 6, data).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(FAILED, "test-team", 6, data).toLiteral(), + ); + }); + }); + + describe("|COMPONENT_LOADED|", () => { + it("should verify invokes correct methods", () => { + // Given + const action = ComponentLoaded.of( + createComponentState(LOADING, "test-team", 11), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(IDLE, "test-team", 7).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(LOADED, "test-team", 11).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(LOADED, "test-team", 11).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(LOADED, "test-team", 11).toLiteral(), + ); + }); }); - describe('Actions::', () => { - describe('|COMPONENT_INIT|', () => { - it('should verify invokes correct methods', () => { - // Given - const action = new ActionStub(COMPONENT_INIT, createComponentState(null)); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(INITIALIZED).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(result).toEqual(createComponentsState(createComponentState(INITIALIZED).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(INITIALIZED).toLiteral()); - }); - }); - - describe('|COMPONENT_IDLE|', () => { - it('should verify invokes correct methods', () => { - // Given - const action = ComponentIdle.of(createComponentState(INITIALIZED, 'test-team', 4)); - getComponentHelperSpy.and.returnValue(createComponentState(INITIALIZED).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(IDLE, 'test-team', 4).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(IDLE, 'test-team', 4).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(IDLE, 'test-team', 4).toLiteral()); - }); - }); - - describe('|COMPONENT_LOADING|', () => { - it('should verify invokes correct methods', () => { - // Given - const action = new ActionStub(COMPONENT_LOADING, createComponentState(IDLE, 'test-team', 4)); - getComponentHelperSpy.and.returnValue(createComponentState(IDLE).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(LOADING, 'test-team', 4).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(LOADING, 'test-team', 4).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(LOADING, 'test-team', 4).toLiteral()); - }); - }); - - describe('|COMPONENT_UPDATE|', () => { - it('should verify invokes correct methods (data from action)', () => { - // Given - const data = new Map([ - ['users', []], - ['cities', {}] - ]); - const action = new ActionStub(COMPONENT_UPDATE, createComponentState(IDLE, 'test-team', 7, data)); - getComponentHelperSpy.and.returnValue(createComponentState(LOADING).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(IDLE, 'test-team', 7, data).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(IDLE, 'test-team', 7, data).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(IDLE, 'test-team', 7, data).toLiteral()); - }); - - it('should verify invokes correct methods (data from store)', () => { - // Given - const data = new Map([ - ['countries', []], - ['payload', {}] - ]); - const action = new ActionStub(COMPONENT_UPDATE, createComponentState(FAILED, 'test-team', 6)); - getComponentHelperSpy.and.returnValue(createComponentState(LOADING, 'test-team', 4, data).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(FAILED, 'test-team', 6, data).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(FAILED, 'test-team', 6, data).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(FAILED, 'test-team', 6, data).toLiteral()); - }); - }); - - describe('|COMPONENT_LOADED|', () => { - it('should verify invokes correct methods', () => { - // Given - const action = ComponentLoaded.of(createComponentState(LOADING, 'test-team', 11)); - getComponentHelperSpy.and.returnValue(createComponentState(IDLE, 'test-team', 7).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(LOADED, 'test-team', 11).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(LOADED, 'test-team', 11).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(LOADED, 'test-team', 11).toLiteral()); - }); - }); - - describe('|COMPONENT_FAILED|', () => { - it('should verify invokes correct methods (data from action)', () => { - // Given - const data = new Map([ - ['data', []], - ['invokes', {}] - ]); - const action = new ActionStub(COMPONENT_FAILED, createComponentState(IDLE, 'test-team', 9, data)); - getComponentHelperSpy.and.returnValue(createComponentState(LOADING).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(FAILED, 'test-team', 9, data).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(FAILED, 'test-team', 9, data).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(FAILED, 'test-team', 9, data).toLiteral()); - }); - - it('should verify invokes correct methods (data from store)', () => { - // Given - const data = new Map([ - ['wifi', []], - ['routers', {}] - ]); - const action = new ActionStub(COMPONENT_FAILED, createComponentState(LOADING, 'test-team', 16)); - getComponentHelperSpy.and.returnValue(createComponentState(LOADING, 'test-team', 14, data).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(FAILED, 'test-team', 16, data).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(FAILED, 'test-team', 16, data).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(FAILED, 'test-team', 16, data).toLiteral()); - }); - }); - - describe('|COMPONENT_CLEAR_DATA|', () => { - it('should verify invokes correct methods', () => { - // Given - const data = new Map([ - ['admins', []], - ['telcos', {}] - ]); - const action = new ActionStub(COMPONENT_CLEAR_DATA, createComponentState(LOADED, 'test-team', 21)); - getComponentHelperSpy.and.returnValue(createComponentState(LOADED, 'test-team', 15, data).toLiteral()); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(LOADING, 'test-team', 21).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(LOADING, 'test-team', 21).toLiteral())); - expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(LOADING, 'test-team', 21).toLiteral()); - }); - }); - - describe('|ROUTER_NAVIGATION|', () => { - it('should verify invokes correct methods', () => { - // Given - const routerNavigationPayload = createRouterNavigationPayload(); - const action = new ActionStub>(ROUTER_NAVIGATION, routerNavigationPayload); - const resetComponentsHelperSpy = spyOn(ComponentsStateHelper.prototype, 'resetComponentStates').and.callFake(CallFake); - getStateHelperSpy.and.returnValue(createComponentsState(createComponentState(IDLE, 'test-team', 17).toLiteral())); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(resetComponentsHelperSpy).toHaveBeenCalledWith(routerNavigationPayload.routerState.routePathSegments); - expect(result).toEqual(createComponentsState(createComponentState(IDLE, 'test-team', 17).toLiteral())); - }); - }); - - describe('|UNKNOWN_ACTION for this Reducer|', () => { - it('should verify will return state as it is', () => { - // Given - const action = new ActionStub('[action] unknown type', {}); - - // When - const result = componentReducer(literalComponentsState, action); - - // Then - expect(result).toBe(literalComponentsState); - }); - }); + describe("|COMPONENT_FAILED|", () => { + it("should verify invokes correct methods (data from action)", () => { + // Given + const data = new Map([ + ["data", []], + ["invokes", {}], + ]); + const action = new ActionStub( + COMPONENT_FAILED, + createComponentState(IDLE, "test-team", 9, data), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(LOADING).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(FAILED, "test-team", 9, data).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(FAILED, "test-team", 9, data).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(FAILED, "test-team", 9, data).toLiteral(), + ); + }); + + it("should verify invokes correct methods (data from store)", () => { + // Given + const data = new Map([ + ["wifi", []], + ["routers", {}], + ]); + const action = new ActionStub( + COMPONENT_FAILED, + createComponentState(LOADING, "test-team", 16), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(LOADING, "test-team", 14, data).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(FAILED, "test-team", 16, data).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(FAILED, "test-team", 16, data).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(FAILED, "test-team", 16, data).toLiteral(), + ); + }); }); + + describe("|COMPONENT_CLEAR_DATA|", () => { + it("should verify invokes correct methods", () => { + // Given + const data = new Map([ + ["admins", []], + ["telcos", {}], + ]); + const action = new ActionStub( + COMPONENT_CLEAR_DATA, + createComponentState(LOADED, "test-team", 21), + ); + getComponentHelperSpy.and.returnValue( + createComponentState(LOADED, "test-team", 15, data).toLiteral(), + ); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(LOADING, "test-team", 21).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith( + id, + routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(LOADING, "test-team", 21).toLiteral(), + ), + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(LOADING, "test-team", 21).toLiteral(), + ); + }); + }); + + describe("|ROUTER_NAVIGATION|", () => { + it("should verify invokes correct methods", () => { + // Given + const routerNavigationPayload = createRouterNavigationPayload(); + const action = new ActionStub>( + ROUTER_NAVIGATION, + routerNavigationPayload, + ); + const resetComponentsHelperSpy = spyOn( + ComponentsStateHelper.prototype, + "resetComponentStates", + ).and.callFake(CallFake); + getStateHelperSpy.and.returnValue( + createComponentsState( + createComponentState(IDLE, "test-team", 17).toLiteral(), + ), + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(resetComponentsHelperSpy).toHaveBeenCalledWith( + routerNavigationPayload.routerState.routePathSegments, + ); + expect(result).toEqual( + createComponentsState( + createComponentState(IDLE, "test-team", 17).toLiteral(), + ), + ); + }); + }); + + describe("|UNKNOWN_ACTION for this Reducer|", () => { + it("should verify will return state as it is", () => { + // Given + const action = new ActionStub("[action] unknown type", {}); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(result).toBe(literalComponentsState); + }); + }); + }); }); class ActionStub extends BaseActionWithPayload { - constructor(type: string, payload: T) { - super(type, payload); - } + constructor(type: string, payload: T) { + super(type, payload); + } } const createComponentState = ( - status: StatusType, - search = '', - navigationId = 3, - data: Map = new Map() + status: StatusType, + search = "", + navigationId = 3, + data: Map = new Map(), ): ComponentState => { - const literalComponentState: LiteralComponentState = { - id: 'testComponent1', - status, - routePath: 'test_entity/15', - routePathSegments: ['test_entity/15'], - navigationId, - search, - data: CollectionsUtil.transformMapToObject(data) - }; - - return { - ...(literalComponentState as any), - data, - copy: CallFake, - toLiteral: () => literalComponentState, - toLiteralCloneDeep: () => literalComponentState - } as ComponentState; + const literalComponentState: LiteralComponentState = { + id: "testComponent1", + status, + routePath: "test_entity/15", + routePathSegments: ["test_entity/15"], + navigationId, + search, + data: CollectionsUtil.transformMapToObject(data), + }; + + return { + ...(literalComponentState as any), + data, + copy: CallFake, + toLiteral: () => literalComponentState, + toLiteralCloneDeep: () => literalComponentState, + } as ComponentState; }; -const createComponentsState = (componentState: LiteralComponentState): LiteralComponentsState => { - return { - components: {}, - routePathSegments: { - 'test_entity/15': { - components: { - testComponent1: componentState - }, - routePathSegments: {} - } - } - }; +const createComponentsState = ( + componentState: LiteralComponentState, +): LiteralComponentsState => { + return { + components: {}, + routePathSegments: { + "test_entity/15": { + components: { + testComponent1: componentState, + }, + routePathSegments: {}, + }, + }, + }; }; -const createRouterNavigationPayload = (): RouterNavigationPayload => { +const createRouterNavigationPayload = + (): RouterNavigationPayload => { return { - routerState: RouteState.of(RouteSegments.of('entity/21'), 'entity/21'), - event: null + routerState: RouteState.of(RouteSegments.of("entity/21"), "entity/21"), + event: null, }; -}; + }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts index 4e6368cccc..62cc098eb9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts @@ -5,30 +5,39 @@ /* eslint-disable arrow-body-style,prefer-arrow/prefer-arrow-functions */ -import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store'; +import { ROUTER_NAVIGATION, RouterNavigationAction } from "@ngrx/router-store"; -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { NavigationActions, RouteSegments, RouteState } from '../../../router'; +import { NavigationActions, RouteSegments, RouteState } from "../../../router"; -import { ComponentsStateHelper, ComponentState, FAILED, IDLE, INITIALIZED, LiteralComponentState, LOADED, LOADING } from '../../model'; +import { + ComponentsStateHelper, + ComponentState, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentState, + LOADED, + LOADING, +} from "../../model"; import { - COMPONENT_CLEAR_DATA, - COMPONENT_FAILED, - COMPONENT_IDLE, - COMPONENT_INIT, - COMPONENT_LOADED, - COMPONENT_LOADING, - COMPONENT_UPDATE, - ComponentActions, - ComponentClearData, - ComponentIdle, - ComponentInit, - ComponentLoaded, - ComponentLoading, - ComponentUpdate -} from '../actions'; + COMPONENT_CLEAR_DATA, + COMPONENT_FAILED, + COMPONENT_IDLE, + COMPONENT_INIT, + COMPONENT_LOADED, + COMPONENT_LOADING, + COMPONENT_UPDATE, + ComponentActions, + ComponentClearData, + ComponentIdle, + ComponentInit, + ComponentLoaded, + ComponentLoading, + ComponentUpdate, +} from "../actions"; const stateHelper = new ComponentsStateHelper(); @@ -36,119 +45,134 @@ const stateHelper = new ComponentsStateHelper(); * ** Reducer for Components Actions. */ export function componentReducer( - state = stateHelper.getState(), - action: ComponentActions | NavigationActions = { type: null, payload: null } + state = stateHelper.getState(), + action: ComponentActions | NavigationActions = { type: null, payload: null }, ) { - let actionComponentState: ComponentState; - let actionLiteralComponentState: LiteralComponentState; - let storeLiteralComponentState: LiteralComponentState; - - stateHelper.setState(state); - - switch (action.type) { - case COMPONENT_INIT: - actionComponentState = (action as ComponentInit).payload; - - stateHelper.updateLiteralComponentState({ - ...actionComponentState.toLiteralCloneDeep(), - status: INITIALIZED - }); - - return stateHelper.getState(); - case COMPONENT_IDLE: - case COMPONENT_LOADING: - actionComponentState = (action as ComponentIdle | ComponentLoading).payload; - storeLiteralComponentState = stateHelper.getLiteralComponentState( - actionComponentState.id, - actionComponentState.routePathSegments - ); - - actionLiteralComponentState = actionComponentState.toLiteralCloneDeep(); - - stateHelper.updateLiteralComponentState({ - ...storeLiteralComponentState, - ...actionLiteralComponentState, - status: action instanceof ComponentIdle ? IDLE : LOADING - }); - - return stateHelper.getState(); - case COMPONENT_UPDATE: - case COMPONENT_LOADED: - actionComponentState = (action as ComponentLoaded | ComponentUpdate).payload; - storeLiteralComponentState = stateHelper.getLiteralComponentState( - actionComponentState.id, - actionComponentState.routePathSegments - ); - - actionLiteralComponentState = actionComponentState.toLiteralCloneDeep(); - - stateHelper.updateLiteralComponentState({ - ...storeLiteralComponentState, - ...actionLiteralComponentState, - status: action instanceof ComponentLoaded ? LOADED : actionComponentState.status, - data: getComponentStateData(actionLiteralComponentState, storeLiteralComponentState) - }); - - return stateHelper.getState(); - case COMPONENT_FAILED: - actionComponentState = (action as ComponentLoaded | ComponentUpdate).payload; - storeLiteralComponentState = stateHelper.getLiteralComponentState( - actionComponentState.id, - actionComponentState.routePathSegments - ); - - actionLiteralComponentState = actionComponentState.toLiteralCloneDeep(); - - stateHelper.updateLiteralComponentState({ - ...storeLiteralComponentState, - ...actionLiteralComponentState, - data: getComponentStateData(actionLiteralComponentState, storeLiteralComponentState), - status: FAILED - }); - - return stateHelper.getState(); - case COMPONENT_CLEAR_DATA: - actionComponentState = (action as ComponentClearData).payload; - storeLiteralComponentState = stateHelper.getLiteralComponentState( - actionComponentState.id, - actionComponentState.routePathSegments - ); - - stateHelper.updateLiteralComponentState({ - ...storeLiteralComponentState, - ...actionComponentState.toLiteralCloneDeep(), - data: {}, - status: LOADING - }); - - return stateHelper.getState(); - case ROUTER_NAVIGATION: - const routeSegments = (action as RouterNavigationAction).payload.routerState.routeSegments; - - if (!(routeSegments instanceof RouteSegments)) { - return state; - } - - stateHelper.resetComponentStates(routeSegments.routePathSegments); - - return stateHelper.getState(); - default: - return state; - } + let actionComponentState: ComponentState; + let actionLiteralComponentState: LiteralComponentState; + let storeLiteralComponentState: LiteralComponentState; + + stateHelper.setState(state); + + switch (action.type) { + case COMPONENT_INIT: + actionComponentState = (action as ComponentInit).payload; + + stateHelper.updateLiteralComponentState({ + ...actionComponentState.toLiteralCloneDeep(), + status: INITIALIZED, + }); + + return stateHelper.getState(); + case COMPONENT_IDLE: + case COMPONENT_LOADING: + actionComponentState = (action as ComponentIdle | ComponentLoading) + .payload; + storeLiteralComponentState = stateHelper.getLiteralComponentState( + actionComponentState.id, + actionComponentState.routePathSegments, + ); + + actionLiteralComponentState = actionComponentState.toLiteralCloneDeep(); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionLiteralComponentState, + status: action instanceof ComponentIdle ? IDLE : LOADING, + }); + + return stateHelper.getState(); + case COMPONENT_UPDATE: + case COMPONENT_LOADED: + actionComponentState = (action as ComponentLoaded | ComponentUpdate) + .payload; + storeLiteralComponentState = stateHelper.getLiteralComponentState( + actionComponentState.id, + actionComponentState.routePathSegments, + ); + + actionLiteralComponentState = actionComponentState.toLiteralCloneDeep(); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionLiteralComponentState, + status: + action instanceof ComponentLoaded + ? LOADED + : actionComponentState.status, + data: getComponentStateData( + actionLiteralComponentState, + storeLiteralComponentState, + ), + }); + + return stateHelper.getState(); + case COMPONENT_FAILED: + actionComponentState = (action as ComponentLoaded | ComponentUpdate) + .payload; + storeLiteralComponentState = stateHelper.getLiteralComponentState( + actionComponentState.id, + actionComponentState.routePathSegments, + ); + + actionLiteralComponentState = actionComponentState.toLiteralCloneDeep(); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionLiteralComponentState, + data: getComponentStateData( + actionLiteralComponentState, + storeLiteralComponentState, + ), + status: FAILED, + }); + + return stateHelper.getState(); + case COMPONENT_CLEAR_DATA: + actionComponentState = (action as ComponentClearData).payload; + storeLiteralComponentState = stateHelper.getLiteralComponentState( + actionComponentState.id, + actionComponentState.routePathSegments, + ); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionComponentState.toLiteralCloneDeep(), + data: {}, + status: LOADING, + }); + + return stateHelper.getState(); + case ROUTER_NAVIGATION: + const routeSegments = (action as RouterNavigationAction) + .payload.routerState.routeSegments; + + if (!(routeSegments instanceof RouteSegments)) { + return state; + } + + stateHelper.resetComponentStates(routeSegments.routePathSegments); + + return stateHelper.getState(); + default: + return state; + } } const getComponentStateData = ( - actionComponentState: LiteralComponentState, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - storeLiteralComponentState: LiteralComponentState + actionComponentState: LiteralComponentState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + storeLiteralComponentState: LiteralComponentState, ): { [key: string]: any } => { - if (CollectionsUtil.isLiteralObjectWithProperties(actionComponentState.data)) { - return actionComponentState.data; - } + if ( + CollectionsUtil.isLiteralObjectWithProperties(actionComponentState.data) + ) { + return actionComponentState.data; + } - if (CollectionsUtil.isDefined(storeLiteralComponentState)) { - return storeLiteralComponentState.data; - } + if (CollectionsUtil.isDefined(storeLiteralComponentState)) { + return storeLiteralComponentState.data; + } - return {}; + return {}; }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts index abbb6b4bde..f0f3726566 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component.reducer'; +export * from "./component.reducer"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/index.ts index d131ee1f6b..2d9c350d61 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './utils'; -export * from './store/model'; +export * from "./utils"; +export * from "./store/model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.spec.ts index 0fb3f28742..0cd6596981 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.spec.ts @@ -3,1477 +3,1634 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpErrorResponse } from "@angular/common/http"; + +import { CallFake } from "../../../../unit-testing"; + +import { CollectionsUtil } from "../../../../utils"; + +import { ErrorRecord } from "../../../../common"; + +import { ErrorStoreImpl, filterErrorRecords } from "./error-store.impl"; + +describe("ErrorStoreImpl", () => { + let errorCodes: string[]; + let errorRecords: ErrorRecord[]; + + beforeEach(() => { + errorCodes = [ + "SomeCode", + "SomeCodeRnd_422", + "SomeCodeRnd_404", + "SomeDng_404", + "SomeDngRbg_500", + "SomeCodeRnd_503", + "SomeCodeRnd_500", + "SomeCode_422", + ]; + errorRecords = [ + { + code: errorCodes[0], + error: new Error("Some Error"), + objectUUID: "SomeObjectUUID_1", + }, + { + code: errorCodes[1], + error: new HttpErrorResponse({ + error: new Error("Some Error 2"), + status: 422, + }), + httpStatusCode: 422, + objectUUID: "SomeObjectUUID_1", + }, + { + code: errorCodes[2], + error: new HttpErrorResponse({ + error: new Error("Some Error 3"), + status: 404, + }), + httpStatusCode: 404, + objectUUID: "SomeObjectUUID_2", + }, + { + code: errorCodes[3], + error: new Error("Some Error 3"), + objectUUID: "SomeObjectUUID_3", + }, + { + code: errorCodes[4], + error: new HttpErrorResponse({ + error: new Error("Some Error 4"), + status: 503, + }), + httpStatusCode: 503, + objectUUID: "SomeObjectUUID_4", + }, + { + code: errorCodes[5], + error: new HttpErrorResponse({ + error: new Error("Some Error 5"), + status: 503, + }), + httpStatusCode: 503, + objectUUID: "SomeObjectUUID_5", + }, + { + code: errorCodes[6], + error: new HttpErrorResponse({ + error: new Error("Some Error 6"), + status: 500, + }), + httpStatusCode: 500, + objectUUID: "SomeObjectUUID_6", + }, + { + code: errorCodes[7], + error: new HttpErrorResponse({ + error: new Error("Some Error 7"), + status: 422, + }), + httpStatusCode: 422, + objectUUID: "SomeObjectUUID_7", + }, + ]; + }); + + it("should verify instance is created", () => { + // When + const instance = new ErrorStoreImpl(); + + // Then + expect(instance).toBeDefined(); + }); + + it("should verify correct value are assigned", () => { + // When + const instance = new ErrorStoreImpl(errorRecords); + + // Then + expect(instance.records).toBe(errorRecords); + expect(instance.changeListeners).toEqual([]); + }); + + it("should verify on Nil or missing parameter default values will be assigned", () => { + // When + const instance1 = new ErrorStoreImpl(); + const instance2 = new ErrorStoreImpl(null); + const instance3 = new ErrorStoreImpl(undefined); + + // Then + expect(instance1.records).toEqual([]); + expect(instance1.changeListeners).toEqual([]); + + expect(instance2.records).toEqual([]); + expect(instance2.changeListeners).toEqual([]); + + expect(instance3.records).toEqual([]); + expect(instance3.changeListeners).toEqual([]); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = ErrorStoreImpl.of(errorRecords); + + // Then + expect(instance).toBeInstanceOf(ErrorStoreImpl); + expect(instance.records).toBe(errorRecords); + expect(instance.changeListeners).toEqual([]); + }); + }); + + describe("|empty|", () => { + it("should verify will create empty instance with default values", () => { + // When + const instance = ErrorStoreImpl.empty(); -import { CallFake } from '../../../../unit-testing'; + // Then + expect(instance).toBeInstanceOf(ErrorStoreImpl); + expect(instance.records).toEqual([]); + expect(instance.changeListeners).toEqual([]); + }); + }); + + describe("|fromLiteral|", () => { + it("should verify will invoke method ErrorStoreImpl.cloneDeepErrorRecords", () => { + // Given + const spyCloneDeepErrorRecords = spyOn( + ErrorStoreImpl, + "cloneDeepErrorRecords", + ).and.callThrough(); + const spyOf = spyOn(ErrorStoreImpl, "of").and.callThrough(); + + // When + const instance = ErrorStoreImpl.fromLiteral(errorRecords); + + // Then + expect(instance.records).toEqual(errorRecords); + expect(spyCloneDeepErrorRecords).toHaveBeenCalledWith(errorRecords); + expect(spyOf).toHaveBeenCalledWith([ + jasmine.any(Object), + jasmine.any(Object), + jasmine.any(Object), + jasmine.any(Object), + jasmine.any(Object), + jasmine.any(Object), + jasmine.any(Object), + jasmine.any(Object), + ]); + }); + }); -import { CollectionsUtil } from '../../../../utils'; + describe("|cloneDeepErrorRecords|", () => { + it("should verify will clone deep provided ErrorRecords literals", () => { + // When + const recordsClonedDeep = + ErrorStoreImpl.cloneDeepErrorRecords(errorRecords); + + // Then + expect(recordsClonedDeep).toEqual(errorRecords); + + expect(recordsClonedDeep).not.toBe(errorRecords); + for (let i = 0; i < recordsClonedDeep.length; i++) { + expect(recordsClonedDeep[i]).not.toBe(errorRecords[i]); + } + }); -import { ErrorRecord } from '../../../../common'; + it("should verify will return empty Array if Nil parameter is provided", () => { + // When + const records1 = ErrorStoreImpl.cloneDeepErrorRecords(null); + const records2 = ErrorStoreImpl.cloneDeepErrorRecords(undefined); -import { ErrorStoreImpl, filterErrorRecords } from './error-store.impl'; + // Then + expect(records1).toEqual([]); + expect(records2).toEqual([]); + }); + }); + }); + }); + + describe("Methods::", () => { + let instance: ErrorStoreImpl; + let spyExecuteChangeListeners: jasmine.Spy; + + // mock times + let dateNow1: number; + let dateNow2: number; + let dateNow3: number; + let dateNow4: number; + let dateNow5: number; + let dateNow6: number; + let dateNow7: number; + let dateNow8: number; -describe('ErrorStoreImpl', () => { - let errorCodes: string[]; - let errorRecords: ErrorRecord[]; + let extraErrorRecords: ErrorRecord[]; beforeEach(() => { - errorCodes = [ - 'SomeCode', - 'SomeCodeRnd_422', - 'SomeCodeRnd_404', - 'SomeDng_404', - 'SomeDngRbg_500', - 'SomeCodeRnd_503', - 'SomeCodeRnd_500', - 'SomeCode_422' - ]; - errorRecords = [ - { - code: errorCodes[0], - error: new Error('Some Error'), - objectUUID: 'SomeObjectUUID_1' - }, - { - code: errorCodes[1], - error: new HttpErrorResponse({ - error: new Error('Some Error 2'), - status: 422 - }), - httpStatusCode: 422, - objectUUID: 'SomeObjectUUID_1' - }, - { - code: errorCodes[2], - error: new HttpErrorResponse({ - error: new Error('Some Error 3'), - status: 404 - }), - httpStatusCode: 404, - objectUUID: 'SomeObjectUUID_2' - }, - { - code: errorCodes[3], - error: new Error('Some Error 3'), - objectUUID: 'SomeObjectUUID_3' - }, - { - code: errorCodes[4], - error: new HttpErrorResponse({ - error: new Error('Some Error 4'), - status: 503 - }), - httpStatusCode: 503, - objectUUID: 'SomeObjectUUID_4' - }, - { - code: errorCodes[5], - error: new HttpErrorResponse({ - error: new Error('Some Error 5'), - status: 503 - }), - httpStatusCode: 503, - objectUUID: 'SomeObjectUUID_5' - }, - { - code: errorCodes[6], - error: new HttpErrorResponse({ - error: new Error('Some Error 6'), - status: 500 - }), - httpStatusCode: 500, - objectUUID: 'SomeObjectUUID_6' - }, - { - code: errorCodes[7], - error: new HttpErrorResponse({ - error: new Error('Some Error 7'), - status: 422 - }), - httpStatusCode: 422, - objectUUID: 'SomeObjectUUID_7' - } - ]; + dateNow1 = Date.now() - 20000; + dateNow2 = Date.now() - 10000; + dateNow3 = Date.now() - 2000; + dateNow4 = Date.now() - 1000; + dateNow5 = Date.now() - 500; + dateNow6 = Date.now() - 250; + dateNow7 = Date.now() - 100; + dateNow8 = Date.now() - 50; + + let counter = 0; + spyOn(CollectionsUtil, "dateNow").and.callFake(() => { + switch (++counter) { + case 1: + case 9: + return dateNow1; + case 2: + case 10: + return dateNow2; + case 3: + case 11: + return dateNow3; + case 4: + case 12: + return dateNow4; + case 5: + case 13: + return dateNow5; + case 6: + case 14: + return dateNow6; + case 7: + case 15: + return dateNow7; + case 8: + case 16: + return dateNow8; + default: + return Date.now() + Math.ceil(Math.random() * 1000); + } + }); + + // @ts-ignore + spyExecuteChangeListeners = spyOn( + ErrorStoreImpl, + "_executeChangeListeners", + ).and.callThrough(); + + instance = new ErrorStoreImpl(); + + extraErrorRecords = [ + { + code: "SomeCodeBrr_110", + error: new HttpErrorResponse({ + error: new Error("Some Error 110"), + status: 404, + }), + httpStatusCode: 404, + objectUUID: "SomeObjectUUID_110", + }, + { + code: "SomeCodeBrr_120", + error: new Error("Some Error 11"), + objectUUID: "SomeObjectUUID_120", + }, + { + code: "SomeCodeBrr_130", + error: new HttpErrorResponse({ + error: new Error("Some Error 130"), + status: 409, + }), + httpStatusCode: 409, + objectUUID: "SomeObjectUUID_130", + }, + ]; }); - it('should verify instance is created', () => { + describe("|hasErrors|", () => { + it("should verify will return true if ErrorRecords exist", () => { + // Given + instance = new ErrorStoreImpl(errorRecords); + // When - const instance = new ErrorStoreImpl(); + const value = instance.hasErrors(); // Then - expect(instance).toBeDefined(); + expect(value).toBeTrue(); + }); + + it("should verify will return false if ErrorRecords does not exist", () => { + // Given + instance = new ErrorStoreImpl(); + + // When + const value = instance.hasErrors(); + + // Then + expect(value).toBeFalse(); + }); }); - it('should verify correct value are assigned', () => { + describe("|hasCode|", () => { + it("should verify will return true if code exist", () => { + // Given + instance = new ErrorStoreImpl(errorRecords); + // When - const instance = new ErrorStoreImpl(errorRecords); + const value1 = instance.hasCode( + "RandomCode", + "UnknownCode", + errorCodes[0], + "NotExistedCode", + errorCodes[1], + ); + const value2 = instance.hasCode(errorCodes[0]); + const value3 = instance.hasCode(errorCodes[1], errorCodes[2]); // Then - expect(instance.records).toBe(errorRecords); - expect(instance.changeListeners).toEqual([]); + expect(value1).toBeTrue(); + expect(value2).toBeTrue(); + expect(value3).toBeTrue(); + }); + + it("should verify will return false if code does not exist", () => { + // Given + const instance1 = new ErrorStoreImpl(errorRecords); + const instance2 = new ErrorStoreImpl(); + + // When + const value1 = instance1.hasCode( + "RandomCode", + "UnknownCode", + "NotExistedCode", + ); + const value2 = instance1.hasCode("RandomCode"); + const value3 = instance2.hasCode( + "RandomCode", + "UnknownCode", + "NotExistedCode", + ); + const value4 = instance2.hasCode("RandomCode"); + + // Then + expect(value1).toBeFalse(); + expect(value2).toBeFalse(); + expect(value3).toBeFalse(); + expect(value4).toBeFalse(); + }); }); - it('should verify on Nil or missing parameter default values will be assigned', () => { + describe("|hasCodePattern|", () => { + it("should verify will return true if codePattern exist", () => { + // Given + instance = new ErrorStoreImpl(errorRecords); + // When - const instance1 = new ErrorStoreImpl(); - const instance2 = new ErrorStoreImpl(null); - const instance3 = new ErrorStoreImpl(undefined); + const value1 = instance.hasCodePattern( + "RandomCode", + "UnknownCode", + null, + "SomeDng", + undefined, + "NotExistedCode", + errorCodes[1], + ); + const value2 = instance.hasCodePattern(`${errorCodes[0]}$`); + const value3 = instance.hasCodePattern(`${errorCodes[1]}$`); + const value4 = instance.hasCodePattern(`SomeCodeRnd`); + const value5 = instance.hasCodePattern(`Code$`); + const value6 = instance.hasCodePattern(`Rnd`); + const value7 = instance.hasCodePattern(`SomeCode`); // Then - expect(instance1.records).toEqual([]); - expect(instance1.changeListeners).toEqual([]); + expect(value1).toBeTrue(); + expect(value2).toBeTrue(); + expect(value3).toBeTrue(); + expect(value4).toBeTrue(); + expect(value5).toBeTrue(); + expect(value6).toBeTrue(); + expect(value7).toBeTrue(); + }); + + it("should verify will return false if codePattern does not exist", () => { + // Given + const instance1 = new ErrorStoreImpl(errorRecords); + const instance2 = new ErrorStoreImpl(); - expect(instance2.records).toEqual([]); - expect(instance2.changeListeners).toEqual([]); + // When + const value1 = instance1.hasCodePattern( + "RandomCode", + undefined, + "UnknownCode", + null, + "NotExistedCode", + ); + const value2 = instance1.hasCodePattern(`${errorCodes[0]}111`); + const value3 = instance1.hasCodePattern(`SomeCode__`); + const value4 = instance2.hasCodePattern(`${errorCodes[1]}$`); + const value5 = instance2.hasCodePattern( + "RandomCode", + null, + "NotExistedCode", + `${errorCodes[1]}$`, + ); + const value6 = instance2.hasCodePattern(`SomeCodeDng`); - expect(instance3.records).toEqual([]); - expect(instance3.changeListeners).toEqual([]); + // Then + expect(value1).toBeFalse(); + expect(value2).toBeFalse(); + expect(value3).toBeFalse(); + expect(value4).toBeFalse(); + expect(value5).toBeFalse(); + expect(value6).toBeFalse(); + }); + + it("should verify will throw/catch error and log to console", () => { + // Given + instance = new ErrorStoreImpl(errorRecords); + const error = new SyntaxError("Unsupported Action"); + spyOn(Array.prototype, "findIndex").and.throwError(error); + const spyConsoleError = spyOn(console, "error").and.callFake(CallFake); + + // When/Then + const found1 = instance.hasCodePattern(errorCodes[2], errorCodes[0]); + expect(found1).toBeFalse(); + const found2 = instance.hasCodePattern("SomeCodeRnd"); + expect(found2).toBeFalse(); + + expect(spyConsoleError).toHaveBeenCalledWith(error); + }); }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = ErrorStoreImpl.of(errorRecords); - - // Then - expect(instance).toBeInstanceOf(ErrorStoreImpl); - expect(instance.records).toBe(errorRecords); - expect(instance.changeListeners).toEqual([]); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with default values', () => { - // When - const instance = ErrorStoreImpl.empty(); - - // Then - expect(instance).toBeInstanceOf(ErrorStoreImpl); - expect(instance.records).toEqual([]); - expect(instance.changeListeners).toEqual([]); - }); - }); - - describe('|fromLiteral|', () => { - it('should verify will invoke method ErrorStoreImpl.cloneDeepErrorRecords', () => { - // Given - const spyCloneDeepErrorRecords = spyOn(ErrorStoreImpl, 'cloneDeepErrorRecords').and.callThrough(); - const spyOf = spyOn(ErrorStoreImpl, 'of').and.callThrough(); - - // When - const instance = ErrorStoreImpl.fromLiteral(errorRecords); - - // Then - expect(instance.records).toEqual(errorRecords); - expect(spyCloneDeepErrorRecords).toHaveBeenCalledWith(errorRecords); - expect(spyOf).toHaveBeenCalledWith([ - jasmine.any(Object), - jasmine.any(Object), - jasmine.any(Object), - jasmine.any(Object), - jasmine.any(Object), - jasmine.any(Object), - jasmine.any(Object), - jasmine.any(Object) - ]); - }); - }); - - describe('|cloneDeepErrorRecords|', () => { - it('should verify will clone deep provided ErrorRecords literals', () => { - // When - const recordsClonedDeep = ErrorStoreImpl.cloneDeepErrorRecords(errorRecords); - - // Then - expect(recordsClonedDeep).toEqual(errorRecords); - - expect(recordsClonedDeep).not.toBe(errorRecords); - for (let i = 0; i < recordsClonedDeep.length; i++) { - expect(recordsClonedDeep[i]).not.toBe(errorRecords[i]); - } - }); - - it('should verify will return empty Array if Nil parameter is provided', () => { - // When - const records1 = ErrorStoreImpl.cloneDeepErrorRecords(null); - const records2 = ErrorStoreImpl.cloneDeepErrorRecords(undefined); - - // Then - expect(records1).toEqual([]); - expect(records2).toEqual([]); - }); - }); - }); + describe("|record|", () => { + it("should verify will record error", () => { + // Then 1 + expect(instance.records).toEqual([]); + + // When/Then 2 + instance.record( + errorRecords[1].code, + errorRecords[1].objectUUID, + errorRecords[1].error, + ); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, + ]); + instance.record(errorRecords[0]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, + { ...errorRecords[0], time: dateNow2 }, + ]); + instance.record(errorRecords[2]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 }, + ]); + instance.record(errorRecords[3].code, errorRecords[3].objectUUID, null); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 }, + { + ...errorRecords[3], + time: dateNow4, + error: null, + httpStatusCode: null, + }, + ]); + instance.record( + errorRecords[4].code, + errorRecords[4].objectUUID, + errorRecords[4].error, + errorRecords[4].httpStatusCode, + ); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 }, + { + ...errorRecords[3], + time: dateNow4, + error: null, + httpStatusCode: null, + }, + { ...errorRecords[4], time: dateNow5 }, + ]); + + // 5 times during record + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(5); + expect(spyExecuteChangeListeners).toHaveBeenCalledWith( + instance, + instance.changeListeners, + ); + }); + + it("should verify will replace existing error for same errorCode and objectUUID and apply new time", () => { + // Then 1 + expect(instance.records).toEqual([]); + + // When/Then 2 + instance.record( + errorRecords[1].code, + errorRecords[1].objectUUID, + errorRecords[1].error, + ); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, + ]); + instance.record(errorRecords[1]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow2 }, + ]); + instance.record(errorRecords[2]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow2 }, + { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 }, + ]); + instance.record(errorRecords[2].code, errorRecords[2].objectUUID, null); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow2 }, + { + ...errorRecords[2], + time: dateNow4, + error: null, + httpStatusCode: null, + }, + ]); + instance.record( + errorRecords[4].code, + errorRecords[4].objectUUID, + errorRecords[4].error, + errorRecords[4].httpStatusCode, + ); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow2 }, + { + ...errorRecords[2], + time: dateNow4, + error: null, + httpStatusCode: null, + }, + { ...errorRecords[4], time: dateNow5 }, + ]); + + // 5 times during record + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(5); + expect(spyExecuteChangeListeners).toHaveBeenCalledWith( + instance, + instance.changeListeners, + ); + }); }); - describe('Methods::', () => { - let instance: ErrorStoreImpl; - let spyExecuteChangeListeners: jasmine.Spy; - - // mock times - let dateNow1: number; - let dateNow2: number; - let dateNow3: number; - let dateNow4: number; - let dateNow5: number; - let dateNow6: number; - let dateNow7: number; - let dateNow8: number; - - let extraErrorRecords: ErrorRecord[]; - - beforeEach(() => { - dateNow1 = Date.now() - 20000; - dateNow2 = Date.now() - 10000; - dateNow3 = Date.now() - 2000; - dateNow4 = Date.now() - 1000; - dateNow5 = Date.now() - 500; - dateNow6 = Date.now() - 250; - dateNow7 = Date.now() - 100; - dateNow8 = Date.now() - 50; - - let counter = 0; - spyOn(CollectionsUtil, 'dateNow').and.callFake(() => { - switch (++counter) { - case 1: - case 9: - return dateNow1; - case 2: - case 10: - return dateNow2; - case 3: - case 11: - return dateNow3; - case 4: - case 12: - return dateNow4; - case 5: - case 13: - return dateNow5; - case 6: - case 14: - return dateNow6; - case 7: - case 15: - return dateNow7; - case 8: - case 16: - return dateNow8; - default: - return Date.now() + Math.ceil(Math.random() * 1000); - } - }); - - // @ts-ignore - spyExecuteChangeListeners = spyOn(ErrorStoreImpl, '_executeChangeListeners').and.callThrough(); - - instance = new ErrorStoreImpl(); - - extraErrorRecords = [ - { - code: 'SomeCodeBrr_110', - error: new HttpErrorResponse({ - error: new Error('Some Error 110'), - status: 404 - }), - httpStatusCode: 404, - objectUUID: 'SomeObjectUUID_110' - }, - { - code: 'SomeCodeBrr_120', - error: new Error('Some Error 11'), - objectUUID: 'SomeObjectUUID_120' - }, - { - code: 'SomeCodeBrr_130', - error: new HttpErrorResponse({ - error: new Error('Some Error 130'), - status: 409 - }), - httpStatusCode: 409, - objectUUID: 'SomeObjectUUID_130' - } - ]; - }); + describe("|removeCode|", () => { + it("should verify will remove existed error codes", () => { + // Given + instance.record(errorRecords[1]); + instance.record(errorRecords[0]); + instance.record(errorRecords[3]); + instance.record(errorRecords[2]); + instance.record(errorRecords[4]); - describe('|hasErrors|', () => { - it('should verify will return true if ErrorRecords exist', () => { - // Given - instance = new ErrorStoreImpl(errorRecords); + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + ]); + + // When/Then 2 + instance.removeCode(errorCodes[0], errorCodes[4]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + ]); + instance.removeCode("SomeCodeRndDng_10"); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + ]); + instance.removeCode(errorCodes[2], errorCodes[0], errorCodes[3]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + ]); + instance.removeCode(errorCodes[1], "SomeCode"); + expect(instance.records).toEqual([]); + + // 5 times during record and 4 times during remove + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(9); + expect(spyExecuteChangeListeners).toHaveBeenCalledWith( + instance, + instance.changeListeners, + ); + }); + + it("should verify will throw/catch error and log to console", () => { + // Given + const error = new SyntaxError("Unsupported Action"); + spyOn(Array.prototype, "includes").and.throwError(error); + const spyConsoleError = spyOn(console, "error").and.callFake(CallFake); - // When - const value = instance.hasErrors(); + instance.record(errorRecords[3]); + instance.record(errorRecords[1]); + instance.record(errorRecords[4]); + instance.record(errorRecords[0]); + instance.record(errorRecords[2]); - // Then - expect(value).toBeTrue(); - }); + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[3], time: dateNow1 }, + { ...errorRecords[1], time: dateNow2 }, + { ...errorRecords[4], time: dateNow3 }, + { ...errorRecords[0], time: dateNow4 }, + { ...errorRecords[2], time: dateNow5 }, + ]); + + // When/Then 2 + instance.removeCode(errorCodes[0], errorCodes[4]); + expect(instance.records).toEqual([ + { ...errorRecords[3], time: dateNow1 }, + { ...errorRecords[1], time: dateNow2 }, + { ...errorRecords[4], time: dateNow3 }, + { ...errorRecords[0], time: dateNow4 }, + { ...errorRecords[2], time: dateNow5 }, + ]); + instance.removeCode(errorCodes[1]); + expect(instance.records).toEqual([ + { ...errorRecords[3], time: dateNow1 }, + { ...errorRecords[1], time: dateNow2 }, + { ...errorRecords[4], time: dateNow3 }, + { ...errorRecords[0], time: dateNow4 }, + { ...errorRecords[2], time: dateNow5 }, + ]); + + expect(spyConsoleError).toHaveBeenCalledWith(error); + // 5 times during record and 2 times during remove + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(7); + }); + }); - it('should verify will return false if ErrorRecords does not exist', () => { - // Given - instance = new ErrorStoreImpl(); + describe("|removeCodePattern|", () => { + beforeEach(() => { + // Given + instance.record(errorRecords[1]); + instance.record(errorRecords[0]); + instance.record(errorRecords[3]); + instance.record(errorRecords[2]); + instance.record(errorRecords[4]); + instance.record(errorRecords[6]); + instance.record(errorRecords[5]); + instance.record(errorRecords[7]); + }); + + it("should verify will remove existed error codes patterns", () => { + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + + // When/Then 2 + instance.removeCodePattern(errorCodes[1], null, errorCodes[3]); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern("SomeCodeRnd$", undefined); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern("SomeCodeRnd"); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern(null, "RandomCodeNotExisting"); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern("SomeCode_"); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[4], time: dateNow5 }, + ]); + instance.removeCodePattern(errorCodes[4], "SomeCode"); + expect(instance.records).toEqual([]); + + // 8 times during record and 6 times during remove + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(14); + expect(spyExecuteChangeListeners).toHaveBeenCalledWith( + instance, + instance.changeListeners, + ); + }); + + it("should verify will remove regex error codes patterns", () => { + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + + // When/Then 2 + instance.removeCodePattern(`SomeCodeRnd_4\\d\\d`); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern("SomeCodeRnd$"); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern("SomeCodeRnd"); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern("RandomCodeNotExisting"); + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern(errorCodes[4], "SomeCode"); + expect(instance.records).toEqual([ + { ...errorRecords[3], time: dateNow3 }, + ]); + instance.removeCodePattern("Dng_404"); + expect(instance.records).toEqual([]); + + // 8 times during record and 6 times during remove + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(14); + expect(spyExecuteChangeListeners).toHaveBeenCalledWith( + instance, + instance.changeListeners, + ); + }); + + it("should verify will throw/catch error and log to console", () => { + // Given + const error = new SyntaxError("Unsupported Action"); + spyOn(Array.prototype, "filter").and.throwError(error); + const spyConsoleError = spyOn(console, "error").and.callFake(CallFake); + + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + + // When/Then 2 + instance.removeCodePattern(errorCodes[4], errorCodes[2]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + instance.removeCodePattern(errorCodes[3]); + expect(instance.records).toEqual([ + { ...errorRecords[1], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[4], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + + expect(spyConsoleError).toHaveBeenCalledWith(error); + expect(spyConsoleError).toHaveBeenCalledTimes(3); + // 8 times during record and 2 times during remove + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(10); + }); + }); - // When - const value = instance.hasErrors(); + describe("|findRecords|", () => { + beforeEach(() => { + // Given + instance.record(errorRecords[4]); + instance.record(errorRecords[0]); + instance.record(errorRecords[2]); + instance.record(errorRecords[3]); + instance.record(errorRecords[1]); + instance.record(errorRecords[7]); + }); + + it("should verify will return Array of ErrorRecord empty or filled with elements if found", () => { + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[4], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[2], time: dateNow3 }, + { ...errorRecords[3], time: dateNow4 }, + { ...errorRecords[1], time: dateNow5 }, + { ...errorRecords[7], time: dateNow6 }, + ]); + + // When/Then 2 + const found1 = instance.findRecords(errorCodes[0], errorCodes[3]); + expect(found1).toEqual([ + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[3], time: dateNow4 }, + ]); + const found2 = instance.findRecords("SomeRandomCode"); + expect(found2).toEqual([]); + const found3 = instance.findRecords(errorCodes[4]); + expect(found3).toEqual([{ ...errorRecords[4], time: dateNow1 }]); + }); + }); - // Then - expect(value).toBeFalse(); - }); - }); + describe("|findRecordsByPattern|", () => { + beforeEach(() => { + // Given + instance.record(errorRecords[0]); + instance.record(errorRecords[4]); + instance.record(errorRecords[3]); + instance.record(errorRecords[2]); + instance.record(errorRecords[1]); + instance.record(errorRecords[7]); + }); + + it("should verify will return Array of ErrorRecord empty or filled with elements if found", () => { + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow1 }, + { ...errorRecords[4], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[1], time: dateNow5 }, + { ...errorRecords[7], time: dateNow6 }, + ]); + + // When/Then 2 + const found1 = instance.findRecordsByPattern( + errorCodes[2], + undefined, + errorCodes[1], + null, + ); + expect(found1).toEqual([ + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[1], time: dateNow5 }, + ]); + const found2 = instance.findRecordsByPattern("SomeCodeRnd"); + expect(found2).toEqual([ + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[1], time: dateNow5 }, + ]); + const found3 = instance.findRecordsByPattern("SomeCodeRnd$"); + expect(found3).toEqual([]); + const found4 = instance.findRecordsByPattern("SomeCode"); + expect(found4).toEqual([ + { ...errorRecords[0], time: dateNow1 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[1], time: dateNow5 }, + { ...errorRecords[7], time: dateNow6 }, + ]); + }); + + it("should verify will throw/catch error and log to console", () => { + // Given + const error = new SyntaxError("Unsupported Action"); + spyOn(Array.prototype, "filter").and.throwError(error); + const spyConsoleError = spyOn(console, "error").and.callFake(CallFake); + + // Then 1 + expect(instance.records).toEqual([ + { ...errorRecords[0], time: dateNow1 }, + { ...errorRecords[4], time: dateNow2 }, + { ...errorRecords[3], time: dateNow3 }, + { ...errorRecords[2], time: dateNow4 }, + { ...errorRecords[1], time: dateNow5 }, + { ...errorRecords[7], time: dateNow6 }, + ]); + + // When/Then 2 + const found1 = instance.findRecordsByPattern( + errorCodes[2], + null, + errorCodes[0], + ); + expect(found1).toEqual([]); + const found2 = instance.findRecordsByPattern("SomeCodeRnd"); + expect(found2).toEqual([]); + + expect(spyConsoleError).toHaveBeenCalledWith(error); + }); + }); - describe('|hasCode|', () => { - it('should verify will return true if code exist', () => { - // Given - instance = new ErrorStoreImpl(errorRecords); - - // When - const value1 = instance.hasCode('RandomCode', 'UnknownCode', errorCodes[0], 'NotExistedCode', errorCodes[1]); - const value2 = instance.hasCode(errorCodes[0]); - const value3 = instance.hasCode(errorCodes[1], errorCodes[2]); - - // Then - expect(value1).toBeTrue(); - expect(value2).toBeTrue(); - expect(value3).toBeTrue(); - }); - - it('should verify will return false if code does not exist', () => { - // Given - const instance1 = new ErrorStoreImpl(errorRecords); - const instance2 = new ErrorStoreImpl(); - - // When - const value1 = instance1.hasCode('RandomCode', 'UnknownCode', 'NotExistedCode'); - const value2 = instance1.hasCode('RandomCode'); - const value3 = instance2.hasCode('RandomCode', 'UnknownCode', 'NotExistedCode'); - const value4 = instance2.hasCode('RandomCode'); - - // Then - expect(value1).toBeFalse(); - expect(value2).toBeFalse(); - expect(value3).toBeFalse(); - expect(value4).toBeFalse(); - }); - }); + describe("|distinctErrorRecords|", () => { + beforeEach(() => { + // Given + instance.record(errorRecords[0]); + instance.record(errorRecords[1]); + instance.record(errorRecords[4]); + instance.record(errorRecords[2]); + instance.record(errorRecords[3]); + instance.record(errorRecords[6]); + instance.record(errorRecords[5]); + instance.record(errorRecords[7]); + }); - describe('|hasCodePattern|', () => { - it('should verify will return true if codePattern exist', () => { - // Given - instance = new ErrorStoreImpl(errorRecords); - - // When - const value1 = instance.hasCodePattern( - 'RandomCode', - 'UnknownCode', - null, - 'SomeDng', - undefined, - 'NotExistedCode', - errorCodes[1] - ); - const value2 = instance.hasCodePattern(`${errorCodes[0]}$`); - const value3 = instance.hasCodePattern(`${errorCodes[1]}$`); - const value4 = instance.hasCodePattern(`SomeCodeRnd`); - const value5 = instance.hasCodePattern(`Code$`); - const value6 = instance.hasCodePattern(`Rnd`); - const value7 = instance.hasCodePattern(`SomeCode`); - - // Then - expect(value1).toBeTrue(); - expect(value2).toBeTrue(); - expect(value3).toBeTrue(); - expect(value4).toBeTrue(); - expect(value5).toBeTrue(); - expect(value6).toBeTrue(); - expect(value7).toBeTrue(); - }); - - it('should verify will return false if codePattern does not exist', () => { - // Given - const instance1 = new ErrorStoreImpl(errorRecords); - const instance2 = new ErrorStoreImpl(); - - // When - const value1 = instance1.hasCodePattern('RandomCode', undefined, 'UnknownCode', null, 'NotExistedCode'); - const value2 = instance1.hasCodePattern(`${errorCodes[0]}111`); - const value3 = instance1.hasCodePattern(`SomeCode__`); - const value4 = instance2.hasCodePattern(`${errorCodes[1]}$`); - const value5 = instance2.hasCodePattern('RandomCode', null, 'NotExistedCode', `${errorCodes[1]}$`); - const value6 = instance2.hasCodePattern(`SomeCodeDng`); - - // Then - expect(value1).toBeFalse(); - expect(value2).toBeFalse(); - expect(value3).toBeFalse(); - expect(value4).toBeFalse(); - expect(value5).toBeFalse(); - expect(value6).toBeFalse(); - }); - - it('should verify will throw/catch error and log to console', () => { - // Given - instance = new ErrorStoreImpl(errorRecords); - const error = new SyntaxError('Unsupported Action'); - spyOn(Array.prototype, 'findIndex').and.throwError(error); - const spyConsoleError = spyOn(console, 'error').and.callFake(CallFake); - - // When/Then - const found1 = instance.hasCodePattern(errorCodes[2], errorCodes[0]); - expect(found1).toBeFalse(); - const found2 = instance.hasCodePattern('SomeCodeRnd'); - expect(found2).toBeFalse(); - - expect(spyConsoleError).toHaveBeenCalledWith(error); - }); - }); + it("should verify will return Array of ErrorRecord from store records that are not equal by value to provided Array of ErrorRecord", () => { + // Given + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[4]); + instance2.record(errorRecords[2]); + instance2.record(errorRecords[3]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[7]); + instance2.record(extraErrorRecords[0]); + instance2.record(extraErrorRecords[2]); + instance2.record(extraErrorRecords[1]); - describe('|record|', () => { - it('should verify will record error', () => { - // Then 1 - expect(instance.records).toEqual([]); - - // When/Then 2 - instance.record(errorRecords[1].code, errorRecords[1].objectUUID, errorRecords[1].error); - expect(instance.records).toEqual([{ ...errorRecords[1], time: dateNow1, httpStatusCode: null }]); - instance.record(errorRecords[0]); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, - { ...errorRecords[0], time: dateNow2 } - ]); - instance.record(errorRecords[2]); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 } - ]); - instance.record(errorRecords[3].code, errorRecords[3].objectUUID, null); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 }, - { ...errorRecords[3], time: dateNow4, error: null, httpStatusCode: null } - ]); - instance.record(errorRecords[4].code, errorRecords[4].objectUUID, errorRecords[4].error, errorRecords[4].httpStatusCode); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1, httpStatusCode: null }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 }, - { ...errorRecords[3], time: dateNow4, error: null, httpStatusCode: null }, - { ...errorRecords[4], time: dateNow5 } - ]); - - // 5 times during record - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(5); - expect(spyExecuteChangeListeners).toHaveBeenCalledWith(instance, instance.changeListeners); - }); - - it('should verify will replace existing error for same errorCode and objectUUID and apply new time', () => { - // Then 1 - expect(instance.records).toEqual([]); - - // When/Then 2 - instance.record(errorRecords[1].code, errorRecords[1].objectUUID, errorRecords[1].error); - expect(instance.records).toEqual([{ ...errorRecords[1], time: dateNow1, httpStatusCode: null }]); - instance.record(errorRecords[1]); - expect(instance.records).toEqual([{ ...errorRecords[1], time: dateNow2 }]); - instance.record(errorRecords[2]); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow2 }, - { ...errorRecords[2], time: dateNow3, httpStatusCode: 404 } - ]); - instance.record(errorRecords[2].code, errorRecords[2].objectUUID, null); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow2 }, - { ...errorRecords[2], time: dateNow4, error: null, httpStatusCode: null } - ]); - instance.record(errorRecords[4].code, errorRecords[4].objectUUID, errorRecords[4].error, errorRecords[4].httpStatusCode); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow2 }, - { ...errorRecords[2], time: dateNow4, error: null, httpStatusCode: null }, - { ...errorRecords[4], time: dateNow5 } - ]); - - // 5 times during record - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(5); - expect(spyExecuteChangeListeners).toHaveBeenCalledWith(instance, instance.changeListeners); - }); - }); + // When + const value1 = instance.distinctErrorRecords(instance2.records); + const value2 = instance.distinctErrorRecords(null); + const value3 = instance.distinctErrorRecords(undefined); + const value4 = instance.distinctErrorRecords([]); + const value5 = instance2.distinctErrorRecords(instance.records); - describe('|removeCode|', () => { - it('should verify will remove existed error codes', () => { - // Given - instance.record(errorRecords[1]); - instance.record(errorRecords[0]); - instance.record(errorRecords[3]); - instance.record(errorRecords[2]); - instance.record(errorRecords[4]); - - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 } - ]); - - // When/Then 2 - instance.removeCode(errorCodes[0], errorCodes[4]); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 } - ]); - instance.removeCode('SomeCodeRndDng_10'); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 } - ]); - instance.removeCode(errorCodes[2], errorCodes[0], errorCodes[3]); - expect(instance.records).toEqual([{ ...errorRecords[1], time: dateNow1 }]); - instance.removeCode(errorCodes[1], 'SomeCode'); - expect(instance.records).toEqual([]); - - // 5 times during record and 4 times during remove - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(9); - expect(spyExecuteChangeListeners).toHaveBeenCalledWith(instance, instance.changeListeners); - }); - - it('should verify will throw/catch error and log to console', () => { - // Given - const error = new SyntaxError('Unsupported Action'); - spyOn(Array.prototype, 'includes').and.throwError(error); - const spyConsoleError = spyOn(console, 'error').and.callFake(CallFake); - - instance.record(errorRecords[3]); - instance.record(errorRecords[1]); - instance.record(errorRecords[4]); - instance.record(errorRecords[0]); - instance.record(errorRecords[2]); - - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[3], time: dateNow1 }, - { ...errorRecords[1], time: dateNow2 }, - { ...errorRecords[4], time: dateNow3 }, - { ...errorRecords[0], time: dateNow4 }, - { ...errorRecords[2], time: dateNow5 } - ]); - - // When/Then 2 - instance.removeCode(errorCodes[0], errorCodes[4]); - expect(instance.records).toEqual([ - { ...errorRecords[3], time: dateNow1 }, - { ...errorRecords[1], time: dateNow2 }, - { ...errorRecords[4], time: dateNow3 }, - { ...errorRecords[0], time: dateNow4 }, - { ...errorRecords[2], time: dateNow5 } - ]); - instance.removeCode(errorCodes[1]); - expect(instance.records).toEqual([ - { ...errorRecords[3], time: dateNow1 }, - { ...errorRecords[1], time: dateNow2 }, - { ...errorRecords[4], time: dateNow3 }, - { ...errorRecords[0], time: dateNow4 }, - { ...errorRecords[2], time: dateNow5 } - ]); - - expect(spyConsoleError).toHaveBeenCalledWith(error); - // 5 times during record and 2 times during remove - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(7); - }); - }); + // Then + expect(value1).toEqual([]); + expect(value2).toEqual([ + instance.records[0], + instance.records[1], + instance.records[2], + instance.records[3], + instance.records[4], + instance.records[5], + instance.records[6], + instance.records[7], + ]); + expect(value3).toEqual([ + instance.records[0], + instance.records[1], + instance.records[2], + instance.records[3], + instance.records[4], + instance.records[5], + instance.records[6], + instance.records[7], + ]); + expect(value4).toEqual([ + instance.records[0], + instance.records[1], + instance.records[2], + instance.records[3], + instance.records[4], + instance.records[5], + instance.records[6], + instance.records[7], + ]); + expect(value5).toEqual([ + instance2.records[8], + instance2.records[9], + instance2.records[10], + ]); + }); + }); - describe('|removeCodePattern|', () => { - beforeEach(() => { - // Given - instance.record(errorRecords[1]); - instance.record(errorRecords[0]); - instance.record(errorRecords[3]); - instance.record(errorRecords[2]); - instance.record(errorRecords[4]); - instance.record(errorRecords[6]); - instance.record(errorRecords[5]); - instance.record(errorRecords[7]); - }); - - it('should verify will remove existed error codes patterns', () => { - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - - // When/Then 2 - instance.removeCodePattern(errorCodes[1], null, errorCodes[3]); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern('SomeCodeRnd$', undefined); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern('SomeCodeRnd'); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern(null, 'RandomCodeNotExisting'); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern('SomeCode_'); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[4], time: dateNow5 } - ]); - instance.removeCodePattern(errorCodes[4], 'SomeCode'); - expect(instance.records).toEqual([]); - - // 8 times during record and 6 times during remove - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(14); - expect(spyExecuteChangeListeners).toHaveBeenCalledWith(instance, instance.changeListeners); - }); - - it('should verify will remove regex error codes patterns', () => { - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - - // When/Then 2 - instance.removeCodePattern(`SomeCodeRnd_4\\d\\d`); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern('SomeCodeRnd$'); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern('SomeCodeRnd'); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern('RandomCodeNotExisting'); - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern(errorCodes[4], 'SomeCode'); - expect(instance.records).toEqual([{ ...errorRecords[3], time: dateNow3 }]); - instance.removeCodePattern('Dng_404'); - expect(instance.records).toEqual([]); - - // 8 times during record and 6 times during remove - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(14); - expect(spyExecuteChangeListeners).toHaveBeenCalledWith(instance, instance.changeListeners); - }); - - it('should verify will throw/catch error and log to console', () => { - // Given - const error = new SyntaxError('Unsupported Action'); - spyOn(Array.prototype, 'filter').and.throwError(error); - const spyConsoleError = spyOn(console, 'error').and.callFake(CallFake); - - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - - // When/Then 2 - instance.removeCodePattern(errorCodes[4], errorCodes[2]); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - instance.removeCodePattern(errorCodes[3]); - expect(instance.records).toEqual([ - { ...errorRecords[1], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[4], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - - expect(spyConsoleError).toHaveBeenCalledWith(error); - expect(spyConsoleError).toHaveBeenCalledTimes(3); - // 8 times during record and 2 times during remove - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(10); - }); - }); + describe("|purge|", () => { + beforeEach(() => { + // Given + instance.record(errorRecords[2]); + instance.record(errorRecords[0]); + instance.record(errorRecords[4]); + instance.record(errorRecords[1]); + instance.record(errorRecords[3]); + instance.record(errorRecords[6]); + instance.record(errorRecords[5]); + instance.record(errorRecords[7]); + }); - describe('|findRecords|', () => { - beforeEach(() => { - // Given - instance.record(errorRecords[4]); - instance.record(errorRecords[0]); - instance.record(errorRecords[2]); - instance.record(errorRecords[3]); - instance.record(errorRecords[1]); - instance.record(errorRecords[7]); - }); - - it('should verify will return Array of ErrorRecord empty or filled with elements if found', () => { - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[4], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[2], time: dateNow3 }, - { ...errorRecords[3], time: dateNow4 }, - { ...errorRecords[1], time: dateNow5 }, - { ...errorRecords[7], time: dateNow6 } - ]); - - // When/Then 2 - const found1 = instance.findRecords(errorCodes[0], errorCodes[3]); - expect(found1).toEqual([ - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[3], time: dateNow4 } - ]); - const found2 = instance.findRecords('SomeRandomCode'); - expect(found2).toEqual([]); - const found3 = instance.findRecords(errorCodes[4]); - expect(found3).toEqual([{ ...errorRecords[4], time: dateNow1 }]); - }); - }); + it("should verify purge will not trigger change listeners and wont add anything if both ErrorStore are equal", () => { + // Given + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[2]); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[4]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[3]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[7]); - describe('|findRecordsByPattern|', () => { - beforeEach(() => { - // Given - instance.record(errorRecords[0]); - instance.record(errorRecords[4]); - instance.record(errorRecords[3]); - instance.record(errorRecords[2]); - instance.record(errorRecords[1]); - instance.record(errorRecords[7]); - }); - - it('should verify will return Array of ErrorRecord empty or filled with elements if found', () => { - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow1 }, - { ...errorRecords[4], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[1], time: dateNow5 }, - { ...errorRecords[7], time: dateNow6 } - ]); - - // When/Then 2 - const found1 = instance.findRecordsByPattern(errorCodes[2], undefined, errorCodes[1], null); - expect(found1).toEqual([ - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[1], time: dateNow5 } - ]); - const found2 = instance.findRecordsByPattern('SomeCodeRnd'); - expect(found2).toEqual([ - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[1], time: dateNow5 } - ]); - const found3 = instance.findRecordsByPattern('SomeCodeRnd$'); - expect(found3).toEqual([]); - const found4 = instance.findRecordsByPattern('SomeCode'); - expect(found4).toEqual([ - { ...errorRecords[0], time: dateNow1 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[1], time: dateNow5 }, - { ...errorRecords[7], time: dateNow6 } - ]); - }); - - it('should verify will throw/catch error and log to console', () => { - // Given - const error = new SyntaxError('Unsupported Action'); - spyOn(Array.prototype, 'filter').and.throwError(error); - const spyConsoleError = spyOn(console, 'error').and.callFake(CallFake); - - // Then 1 - expect(instance.records).toEqual([ - { ...errorRecords[0], time: dateNow1 }, - { ...errorRecords[4], time: dateNow2 }, - { ...errorRecords[3], time: dateNow3 }, - { ...errorRecords[2], time: dateNow4 }, - { ...errorRecords[1], time: dateNow5 }, - { ...errorRecords[7], time: dateNow6 } - ]); - - // When/Then 2 - const found1 = instance.findRecordsByPattern(errorCodes[2], null, errorCodes[0]); - expect(found1).toEqual([]); - const found2 = instance.findRecordsByPattern('SomeCodeRnd'); - expect(found2).toEqual([]); - - expect(spyConsoleError).toHaveBeenCalledWith(error); - }); - }); + // When + instance.purge(instance2); - describe('|distinctErrorRecords|', () => { - beforeEach(() => { - // Given - instance.record(errorRecords[0]); - instance.record(errorRecords[1]); - instance.record(errorRecords[4]); - instance.record(errorRecords[2]); - instance.record(errorRecords[3]); - instance.record(errorRecords[6]); - instance.record(errorRecords[5]); - instance.record(errorRecords[7]); - }); - - it('should verify will return Array of ErrorRecord from store records that are not equal by value to provided Array of ErrorRecord', () => { - // Given - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[4]); - instance2.record(errorRecords[2]); - instance2.record(errorRecords[3]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[7]); - instance2.record(extraErrorRecords[0]); - instance2.record(extraErrorRecords[2]); - instance2.record(extraErrorRecords[1]); - - // When - const value1 = instance.distinctErrorRecords(instance2.records); - const value2 = instance.distinctErrorRecords(null); - const value3 = instance.distinctErrorRecords(undefined); - const value4 = instance.distinctErrorRecords([]); - const value5 = instance2.distinctErrorRecords(instance.records); - - // Then - expect(value1).toEqual([]); - expect(value2).toEqual([ - instance.records[0], - instance.records[1], - instance.records[2], - instance.records[3], - instance.records[4], - instance.records[5], - instance.records[6], - instance.records[7] - ]); - expect(value3).toEqual([ - instance.records[0], - instance.records[1], - instance.records[2], - instance.records[3], - instance.records[4], - instance.records[5], - instance.records[6], - instance.records[7] - ]); - expect(value4).toEqual([ - instance.records[0], - instance.records[1], - instance.records[2], - instance.records[3], - instance.records[4], - instance.records[5], - instance.records[6], - instance.records[7] - ]); - expect(value5).toEqual([instance2.records[8], instance2.records[9], instance2.records[10]]); - }); - }); + // Then + expect(instance.records).toEqual([ + { ...errorRecords[2], time: dateNow1 }, + { ...errorRecords[0], time: dateNow2 }, + { ...errorRecords[4], time: dateNow3 }, + { ...errorRecords[1], time: dateNow4 }, + { ...errorRecords[3], time: dateNow5 }, + { ...errorRecords[6], time: dateNow6 }, + { ...errorRecords[5], time: dateNow7 }, + { ...errorRecords[7], time: dateNow8 }, + ]); + // 16 times during record, 8 for instance and 8 for instance2 + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(16); + }); + + it("should verify purge will trigger change listeners and will add ErrorRecords from injected ErrorStore to invoker", () => { + // Given + const instance2 = new ErrorStoreImpl(); + instance2.record(extraErrorRecords[2]); + instance2.record(extraErrorRecords[0]); + instance2.record(extraErrorRecords[1]); - describe('|purge|', () => { - beforeEach(() => { - // Given - instance.record(errorRecords[2]); - instance.record(errorRecords[0]); - instance.record(errorRecords[4]); - instance.record(errorRecords[1]); - instance.record(errorRecords[3]); - instance.record(errorRecords[6]); - instance.record(errorRecords[5]); - instance.record(errorRecords[7]); - }); - - it('should verify purge will not trigger change listeners and wont add anything if both ErrorStore are equal', () => { - // Given - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[2]); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[4]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[3]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[7]); - - // When - instance.purge(instance2); - - // Then - expect(instance.records).toEqual([ - { ...errorRecords[2], time: dateNow1 }, - { ...errorRecords[0], time: dateNow2 }, - { ...errorRecords[4], time: dateNow3 }, - { ...errorRecords[1], time: dateNow4 }, - { ...errorRecords[3], time: dateNow5 }, - { ...errorRecords[6], time: dateNow6 }, - { ...errorRecords[5], time: dateNow7 }, - { ...errorRecords[7], time: dateNow8 } - ]); - // 16 times during record, 8 for instance and 8 for instance2 - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(16); - }); - - it('should verify purge will trigger change listeners and will add ErrorRecords from injected ErrorStore to invoker', () => { - // Given - const instance2 = new ErrorStoreImpl(); - instance2.record(extraErrorRecords[2]); - instance2.record(extraErrorRecords[0]); - instance2.record(extraErrorRecords[1]); - - // When - instance.purge(instance2); - - // Then - expect(instance.records).toEqual([ - { ...extraErrorRecords[2], time: dateNow1 }, - { ...extraErrorRecords[0], time: dateNow2 }, - { ...extraErrorRecords[1], time: dateNow3 } - ]); - // 11 times during record, 8 for instance and 3 for instance2 and 1 for purge - expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(12); - }); - }); + // When + instance.purge(instance2); - describe('|onChange|', () => { - it('should verify will add listener for change in local repository', () => { - // Given - const listener1: (store: ErrorStoreImpl) => void = (store: ErrorStoreImpl) => { - expect(store).toBe(instance); - }; - const listener2: (store: ErrorStoreImpl) => void = (store: ErrorStoreImpl) => { - expect(store).toBe(instance); - }; - const listener3: (store: ErrorStoreImpl) => void = null; - const listener4: (store: ErrorStoreImpl) => void = undefined; - - // When - instance.onChange(listener1); - instance.onChange(listener2); - instance.onChange(listener3); - instance.onChange(listener4); - - // Then - expect(instance.changeListeners.length).toEqual(2); - expect(instance.changeListeners[0]).toBe(listener1); - expect(instance.changeListeners[1]).toBe(listener2); - }); - - it('should verify attached listeners are executed on ErrorRecord recording', () => { - // Given - const error = new Error('Thrown Error Executed Listener'); - const listener1 = jasmine.createSpy<(_store: ErrorStoreImpl) => void>('listener1').and.callFake(CallFake); - const listener2 = jasmine.createSpy<(_store: ErrorStoreImpl) => void>('listener2').and.throwError(error); - const spyConsoleError = spyOn(console, 'error').and.callFake(CallFake); - instance.onChange(listener1); - instance.onChange(listener2); - - // When - instance.record(errorRecords[0]); - - // Then - expect(listener1).toHaveBeenCalledWith(instance); - expect(listener2).toHaveBeenCalledWith(instance); - expect(spyConsoleError).toHaveBeenCalledWith(`Taurus ErrorStore failed to execute change listeners`, error); - }); - }); + // Then + expect(instance.records).toEqual([ + { ...extraErrorRecords[2], time: dateNow1 }, + { ...extraErrorRecords[0], time: dateNow2 }, + { ...extraErrorRecords[1], time: dateNow3 }, + ]); + // 11 times during record, 8 for instance and 3 for instance2 and 1 for purge + expect(spyExecuteChangeListeners).toHaveBeenCalledTimes(12); + }); + }); - describe('|dispose|', () => { - it('should verify will invoke correct methods', () => { - // Given - const listener1: (store: ErrorStoreImpl) => void = (_store: ErrorStoreImpl) => { - // No-op. - }; - const listener2: (store: ErrorStoreImpl) => void = (_store: ErrorStoreImpl) => { - // No-op. - }; - - instance = new ErrorStoreImpl(errorRecords); - instance.onChange(listener1); - instance.onChange(listener2); - - const spyClear = spyOn(instance, 'clear').and.callThrough(); - - // Then 1 - expect(instance.records).toEqual(errorRecords); - expect(instance.changeListeners).toEqual([listener1, listener2]); - - // When - instance.dispose(); - - // Then 2 - expect(spyClear).toHaveBeenCalledTimes(1); - expect(instance.records).toEqual([]); - expect(instance.changeListeners).toEqual([]); - }); - }); + describe("|onChange|", () => { + it("should verify will add listener for change in local repository", () => { + // Given + const listener1: (store: ErrorStoreImpl) => void = ( + store: ErrorStoreImpl, + ) => { + expect(store).toBe(instance); + }; + const listener2: (store: ErrorStoreImpl) => void = ( + store: ErrorStoreImpl, + ) => { + expect(store).toBe(instance); + }; + const listener3: (store: ErrorStoreImpl) => void = null; + const listener4: (store: ErrorStoreImpl) => void = undefined; - describe('|clear|', () => { - it('should verify will clear ErrorRecord items from records', () => { - instance = new ErrorStoreImpl(extraErrorRecords); + // When + instance.onChange(listener1); + instance.onChange(listener2); + instance.onChange(listener3); + instance.onChange(listener4); - // Then 1 - expect(instance.records).toEqual(extraErrorRecords); + // Then + expect(instance.changeListeners.length).toEqual(2); + expect(instance.changeListeners[0]).toBe(listener1); + expect(instance.changeListeners[1]).toBe(listener2); + }); - // When - instance.clear(); + it("should verify attached listeners are executed on ErrorRecord recording", () => { + // Given + const error = new Error("Thrown Error Executed Listener"); + const listener1 = jasmine + .createSpy<(_store: ErrorStoreImpl) => void>("listener1") + .and.callFake(CallFake); + const listener2 = jasmine + .createSpy<(_store: ErrorStoreImpl) => void>("listener2") + .and.throwError(error); + const spyConsoleError = spyOn(console, "error").and.callFake(CallFake); + instance.onChange(listener1); + instance.onChange(listener2); - // Then 2 - expect(instance.records).toEqual([]); - }); - }); + // When + instance.record(errorRecords[0]); - describe('|toLiteral|', () => { - it('should verify will return Array of ErrorRecord shallow cloned', () => { - instance = new ErrorStoreImpl(extraErrorRecords); + // Then + expect(listener1).toHaveBeenCalledWith(instance); + expect(listener2).toHaveBeenCalledWith(instance); + expect(spyConsoleError).toHaveBeenCalledWith( + `Taurus ErrorStore failed to execute change listeners`, + error, + ); + }); + }); - // Then 1 - expect(instance.records).toEqual(extraErrorRecords); + describe("|dispose|", () => { + it("should verify will invoke correct methods", () => { + // Given + const listener1: (store: ErrorStoreImpl) => void = ( + _store: ErrorStoreImpl, + ) => { + // No-op. + }; + const listener2: (store: ErrorStoreImpl) => void = ( + _store: ErrorStoreImpl, + ) => { + // No-op. + }; + + instance = new ErrorStoreImpl(errorRecords); + instance.onChange(listener1); + instance.onChange(listener2); + + const spyClear = spyOn(instance, "clear").and.callThrough(); + + // Then 1 + expect(instance.records).toEqual(errorRecords); + expect(instance.changeListeners).toEqual([listener1, listener2]); - // When - const literals = instance.toLiteral(); + // When + instance.dispose(); - // Then 2 - expect(literals).toEqual(extraErrorRecords); - expect(literals).not.toBe(extraErrorRecords); - for (let i = 0; i < literals.length; i++) { - expect(literals[i]).toBe(extraErrorRecords[i]); - } - }); - }); + // Then 2 + expect(spyClear).toHaveBeenCalledTimes(1); + expect(instance.records).toEqual([]); + expect(instance.changeListeners).toEqual([]); + }); + }); - describe('|toLiteralCloneDeep|', () => { - it('should verify will return Array of ErrorRecord deep cloned', () => { - instance = new ErrorStoreImpl(extraErrorRecords); + describe("|clear|", () => { + it("should verify will clear ErrorRecord items from records", () => { + instance = new ErrorStoreImpl(extraErrorRecords); - // Then 1 - expect(instance.records).toEqual(extraErrorRecords); + // Then 1 + expect(instance.records).toEqual(extraErrorRecords); - // When - const literals = instance.toLiteralCloneDeep(); + // When + instance.clear(); - // Then 2 - expect(literals).toEqual(extraErrorRecords); - expect(literals).not.toBe(extraErrorRecords); - for (let i = 0; i < literals.length; i++) { - expect(literals[i]).not.toBe(extraErrorRecords[i]); - } - }); - }); + // Then 2 + expect(instance.records).toEqual([]); + }); + }); - describe('|copy|', () => { - it('should verify will return new instance of ErrorStoreImpl with existing ErrorRecord', () => { - instance = new ErrorStoreImpl(extraErrorRecords); + describe("|toLiteral|", () => { + it("should verify will return Array of ErrorRecord shallow cloned", () => { + instance = new ErrorStoreImpl(extraErrorRecords); - // Then 1 - expect(instance.records).toEqual(extraErrorRecords); + // Then 1 + expect(instance.records).toEqual(extraErrorRecords); - // When - const newInstance = instance.copy(); + // When + const literals = instance.toLiteral(); + + // Then 2 + expect(literals).toEqual(extraErrorRecords); + expect(literals).not.toBe(extraErrorRecords); + for (let i = 0; i < literals.length; i++) { + expect(literals[i]).toBe(extraErrorRecords[i]); + } + }); + }); - // Then 2 - expect(newInstance).toBeInstanceOf(ErrorStoreImpl); - expect(newInstance.records).toEqual(extraErrorRecords); - expect(newInstance.records).not.toBe(extraErrorRecords); - for (let i = 0; i < newInstance.records.length; i++) { - expect(newInstance.records[i]).toBe(extraErrorRecords[i]); - } - }); - }); + describe("|toLiteralCloneDeep|", () => { + it("should verify will return Array of ErrorRecord deep cloned", () => { + instance = new ErrorStoreImpl(extraErrorRecords); - describe('|equals|', () => { - it('should verify will return true when both instances of ErrorStoreImpl have same content using deep comparison', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - instance1.record(errorRecords[3]); - instance1.record(errorRecords[4]); - instance1.record(errorRecords[5]); - instance1.record(errorRecords[6]); - instance1.record(errorRecords[7]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[2]); - instance2.record(errorRecords[3]); - instance2.record(errorRecords[4]); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[7]); - - const instance3 = new ErrorStoreImpl(); - const instance4 = new ErrorStoreImpl(); - - // When - const areEqual1 = instance1.equals(instance2); - const areEqual2 = instance3.equals(instance3); - - // Then 2 - expect(areEqual1).toBeTrue(); - expect(instance1.records).toEqual(instance2.records); - expect(instance1.records).not.toBe(instance2.records); - for (let i = 0; i < instance1.records.length; i++) { - expect(instance1.records[i]).not.toBe(instance2.records[i]); - } - - expect(areEqual2).toBeTrue(); - expect(instance3.records).toEqual(instance4.records); - }); - - it('should verify will return false when injected ErrorStoreImpl is Nil', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - - // When - const areEqual1 = instance1.equals(null); - const areEqual2 = instance1.equals(undefined); - - // Then - expect(areEqual1).toBeFalse(); - expect(areEqual2).toBeFalse(); - }); - - it('should verify will return false when both instances of ErrorStoreImpl have different ErrorRecord size', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(extraErrorRecords[0]); - instance2.record(extraErrorRecords[1]); - - // When - const areEqual1 = instance1.equals(instance2); - - // Then 2 - expect(areEqual1).toBeFalse(); - expect(instance1.records).not.toEqual(instance2.records); - }); - - it('should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one code is different', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - instance1.record(errorRecords[3]); - instance1.record(errorRecords[4]); - instance1.record(errorRecords[5]); - instance1.record(errorRecords[6]); - instance1.record(errorRecords[7]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[2]); - instance2.record(extraErrorRecords[0]); - instance2.record(errorRecords[4]); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[7]); - - // When - const areEqual1 = instance1.equals(instance2); - - // Then 2 - expect(areEqual1).toBeFalse(); - expect(instance1.records).not.toEqual(instance2.records); - }); - - it('should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one objectUUID is different', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - instance1.record(errorRecords[3]); - instance1.record(errorRecords[4]); - instance1.record(errorRecords[5]); - instance1.record(errorRecords[6]); - instance1.record(errorRecords[7]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[2]); - instance2.record(errorRecords[3]); - instance2.record(errorRecords[4]); - instance2.record({ ...errorRecords[5], objectUUID: 'newObjectUUID_not_overlap' }); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[7]); - - // When - const areEqual1 = instance1.equals(instance2); - - // Then 2 - expect(areEqual1).toBeFalse(); - expect(instance1.records).not.toEqual(instance2.records); - }); - - it('should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one time is different', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - instance1.record(errorRecords[3]); - instance1.record(errorRecords[4]); - instance1.record(errorRecords[5]); - instance1.record(errorRecords[6]); - instance1.record(errorRecords[7]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[2]); - instance2.record(errorRecords[3]); - instance2.record(errorRecords[4]); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[7]); - // extra record that applies new time for record 6 - instance2.record(errorRecords[6]); - - // When - const areEqual1 = instance1.equals(instance2); - - // Then 2 - expect(areEqual1).toBeFalse(); - expect(instance1.records).not.toEqual(instance2.records); - }); - - it('should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one error is different v1', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - instance1.record(errorRecords[3]); - instance1.record(errorRecords[4]); - instance1.record(errorRecords[5]); - instance1.record(errorRecords[6]); - instance1.record(errorRecords[7]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record({ ...errorRecords[2], error: new Error('New Error Thrown not overlap') }); - instance2.record(errorRecords[3]); - instance2.record(errorRecords[4]); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[7]); - - // When - const areEqual1 = instance1.equals(instance2); - - // Then 2 - expect(areEqual1).toBeFalse(); - expect(instance1.records).not.toEqual(instance2.records); - }); - - it('should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one error is different v2', () => { - // Given - const instance1 = new ErrorStoreImpl(); - instance1.record(errorRecords[0]); - instance1.record(errorRecords[1]); - instance1.record(errorRecords[2]); - instance1.record(errorRecords[3]); - instance1.record(errorRecords[4]); - instance1.record(errorRecords[5]); - instance1.record(errorRecords[6]); - instance1.record(errorRecords[7]); - - const instance2 = new ErrorStoreImpl(); - instance2.record(errorRecords[0]); - instance2.record(errorRecords[1]); - instance2.record(errorRecords[2]); - instance2.record(errorRecords[3]); - instance2.record({ ...errorRecords[4], httpStatusCode: 401 }); - instance2.record(errorRecords[5]); - instance2.record(errorRecords[6]); - instance2.record(errorRecords[7]); - - // When - const areEqual1 = instance1.equals(instance2); - - // Then 2 - expect(areEqual1).toBeFalse(); - expect(instance1.records).not.toEqual(instance2.records); - }); - }); + // Then 1 + expect(instance.records).toEqual(extraErrorRecords); + + // When + const literals = instance.toLiteralCloneDeep(); + + // Then 2 + expect(literals).toEqual(extraErrorRecords); + expect(literals).not.toBe(extraErrorRecords); + for (let i = 0; i < literals.length; i++) { + expect(literals[i]).not.toBe(extraErrorRecords[i]); + } + }); }); -}); -describe('filterErrorRecords', () => { - let errorCodes: string[]; - let errorRecords: ErrorRecord[]; + describe("|copy|", () => { + it("should verify will return new instance of ErrorStoreImpl with existing ErrorRecord", () => { + instance = new ErrorStoreImpl(extraErrorRecords); - // mock times - let dateNow1: number; - let dateNow2: number; - let dateNow3: number; - let dateNow4: number; - let dateNow5: number; + // Then 1 + expect(instance.records).toEqual(extraErrorRecords); - beforeEach(() => { - errorCodes = ['SomeCd', 'SomeCdRnd_1', 'SomeCdRnd_2', 'SomeBng_3', 'SomeBngRbg_4']; - errorRecords = [ - { - code: errorCodes[0], - error: new Error('Some Error'), - objectUUID: 'SomeObjectUUID_20' - }, - { - code: errorCodes[1], - error: new Error('Some Error 100'), - objectUUID: 'SomeObjectUUID_20' - }, - { - code: errorCodes[2], - error: new HttpErrorResponse({ - error: new Error('Some Error 200'), - status: 422 - }), - httpStatusCode: 422, - objectUUID: 'SomeObjectUUID_30' - }, - { - code: errorCodes[3], - error: new Error('Some Error 300'), - objectUUID: 'SomeObjectUUID_40' - }, - { - code: errorCodes[4], - error: new HttpErrorResponse({ - error: new Error('Some Error 400'), - status: 500 - }), - httpStatusCode: 500, - objectUUID: 'SomeObjectUUID_50' - } - ]; - - dateNow1 = Date.now() - 35000; - dateNow2 = Date.now() - 30000; - dateNow3 = Date.now() - 25000; - dateNow4 = Date.now() - 20000; - dateNow5 = Date.now() - 15000; - - let counter = 0; - spyOn(CollectionsUtil, 'dateNow').and.callFake(() => { - switch (++counter) { - case 1: - return dateNow1; - case 2: - return dateNow2; - case 3: - return dateNow3; - case 4: - return dateNow4; - case 5: - return dateNow5; - default: - return Date.now() + Math.ceil(Math.random() * 1000); - } - }); + // When + const newInstance = instance.copy(); + + // Then 2 + expect(newInstance).toBeInstanceOf(ErrorStoreImpl); + expect(newInstance.records).toEqual(extraErrorRecords); + expect(newInstance.records).not.toBe(extraErrorRecords); + for (let i = 0; i < newInstance.records.length; i++) { + expect(newInstance.records[i]).toBe(extraErrorRecords[i]); + } + }); }); - it('should verify filters Array of ErrorRecord correctly', () => { + describe("|equals|", () => { + it("should verify will return true when both instances of ErrorStoreImpl have same content using deep comparison", () => { // Given - const instance = new ErrorStoreImpl(); - instance.record(errorRecords[0]); - instance.record(errorRecords[1]); - instance.record(errorRecords[2]); - instance.record(errorRecords[3]); - instance.record(errorRecords[4]); + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + instance1.record(errorRecords[3]); + instance1.record(errorRecords[4]); + instance1.record(errorRecords[5]); + instance1.record(errorRecords[6]); + instance1.record(errorRecords[7]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[2]); + instance2.record(errorRecords[3]); + instance2.record(errorRecords[4]); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[7]); + + const instance3 = new ErrorStoreImpl(); + const instance4 = new ErrorStoreImpl(); // When - const filteredErrorRecords1 = filterErrorRecords(instance.records, [errorCodes[0], errorCodes[4]]); - const filteredErrorRecords2 = filterErrorRecords(instance.records, [errorCodes[4]], ['SomeCdRnd', null]); - const filteredErrorRecords3 = filterErrorRecords(instance.records, [], ['SomeBng$', undefined]); - const filteredErrorRecords4 = filterErrorRecords(instance.records, null, null); - const filteredErrorRecords5 = filterErrorRecords(instance.records); + const areEqual1 = instance1.equals(instance2); + const areEqual2 = instance3.equals(instance3); + + // Then 2 + expect(areEqual1).toBeTrue(); + expect(instance1.records).toEqual(instance2.records); + expect(instance1.records).not.toBe(instance2.records); + for (let i = 0; i < instance1.records.length; i++) { + expect(instance1.records[i]).not.toBe(instance2.records[i]); + } + + expect(areEqual2).toBeTrue(); + expect(instance3.records).toEqual(instance4.records); + }); + + it("should verify will return false when injected ErrorStoreImpl is Nil", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + + // When + const areEqual1 = instance1.equals(null); + const areEqual2 = instance1.equals(undefined); // Then - expect(filteredErrorRecords1).toEqual([instance.records[4], instance.records[0]]); - expect(filteredErrorRecords2).toEqual([instance.records[4], instance.records[2], instance.records[1]]); - expect(filteredErrorRecords3).toEqual([]); - expect(filteredErrorRecords4).toEqual([]); - expect(filteredErrorRecords5).toEqual([]); + expect(areEqual1).toBeFalse(); + expect(areEqual2).toBeFalse(); + }); + + it("should verify will return false when both instances of ErrorStoreImpl have different ErrorRecord size", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(extraErrorRecords[0]); + instance2.record(extraErrorRecords[1]); + + // When + const areEqual1 = instance1.equals(instance2); + + // Then 2 + expect(areEqual1).toBeFalse(); + expect(instance1.records).not.toEqual(instance2.records); + }); + + it("should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one code is different", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + instance1.record(errorRecords[3]); + instance1.record(errorRecords[4]); + instance1.record(errorRecords[5]); + instance1.record(errorRecords[6]); + instance1.record(errorRecords[7]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[2]); + instance2.record(extraErrorRecords[0]); + instance2.record(errorRecords[4]); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[7]); + + // When + const areEqual1 = instance1.equals(instance2); + + // Then 2 + expect(areEqual1).toBeFalse(); + expect(instance1.records).not.toEqual(instance2.records); + }); + + it("should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one objectUUID is different", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + instance1.record(errorRecords[3]); + instance1.record(errorRecords[4]); + instance1.record(errorRecords[5]); + instance1.record(errorRecords[6]); + instance1.record(errorRecords[7]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[2]); + instance2.record(errorRecords[3]); + instance2.record(errorRecords[4]); + instance2.record({ + ...errorRecords[5], + objectUUID: "newObjectUUID_not_overlap", + }); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[7]); + + // When + const areEqual1 = instance1.equals(instance2); + + // Then 2 + expect(areEqual1).toBeFalse(); + expect(instance1.records).not.toEqual(instance2.records); + }); + + it("should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one time is different", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + instance1.record(errorRecords[3]); + instance1.record(errorRecords[4]); + instance1.record(errorRecords[5]); + instance1.record(errorRecords[6]); + instance1.record(errorRecords[7]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[2]); + instance2.record(errorRecords[3]); + instance2.record(errorRecords[4]); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[7]); + // extra record that applies new time for record 6 + instance2.record(errorRecords[6]); + + // When + const areEqual1 = instance1.equals(instance2); + + // Then 2 + expect(areEqual1).toBeFalse(); + expect(instance1.records).not.toEqual(instance2.records); + }); + + it("should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one error is different v1", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + instance1.record(errorRecords[3]); + instance1.record(errorRecords[4]); + instance1.record(errorRecords[5]); + instance1.record(errorRecords[6]); + instance1.record(errorRecords[7]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record({ + ...errorRecords[2], + error: new Error("New Error Thrown not overlap"), + }); + instance2.record(errorRecords[3]); + instance2.record(errorRecords[4]); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[7]); + + // When + const areEqual1 = instance1.equals(instance2); + + // Then 2 + expect(areEqual1).toBeFalse(); + expect(instance1.records).not.toEqual(instance2.records); + }); + + it("should verify will return false when both instances of ErrorStoreImpl have same ErrorRecord size but for one error is different v2", () => { + // Given + const instance1 = new ErrorStoreImpl(); + instance1.record(errorRecords[0]); + instance1.record(errorRecords[1]); + instance1.record(errorRecords[2]); + instance1.record(errorRecords[3]); + instance1.record(errorRecords[4]); + instance1.record(errorRecords[5]); + instance1.record(errorRecords[6]); + instance1.record(errorRecords[7]); + + const instance2 = new ErrorStoreImpl(); + instance2.record(errorRecords[0]); + instance2.record(errorRecords[1]); + instance2.record(errorRecords[2]); + instance2.record(errorRecords[3]); + instance2.record({ ...errorRecords[4], httpStatusCode: 401 }); + instance2.record(errorRecords[5]); + instance2.record(errorRecords[6]); + instance2.record(errorRecords[7]); + + // When + const areEqual1 = instance1.equals(instance2); + + // Then 2 + expect(areEqual1).toBeFalse(); + expect(instance1.records).not.toEqual(instance2.records); + }); + }); + }); +}); + +describe("filterErrorRecords", () => { + let errorCodes: string[]; + let errorRecords: ErrorRecord[]; + + // mock times + let dateNow1: number; + let dateNow2: number; + let dateNow3: number; + let dateNow4: number; + let dateNow5: number; + + beforeEach(() => { + errorCodes = [ + "SomeCd", + "SomeCdRnd_1", + "SomeCdRnd_2", + "SomeBng_3", + "SomeBngRbg_4", + ]; + errorRecords = [ + { + code: errorCodes[0], + error: new Error("Some Error"), + objectUUID: "SomeObjectUUID_20", + }, + { + code: errorCodes[1], + error: new Error("Some Error 100"), + objectUUID: "SomeObjectUUID_20", + }, + { + code: errorCodes[2], + error: new HttpErrorResponse({ + error: new Error("Some Error 200"), + status: 422, + }), + httpStatusCode: 422, + objectUUID: "SomeObjectUUID_30", + }, + { + code: errorCodes[3], + error: new Error("Some Error 300"), + objectUUID: "SomeObjectUUID_40", + }, + { + code: errorCodes[4], + error: new HttpErrorResponse({ + error: new Error("Some Error 400"), + status: 500, + }), + httpStatusCode: 500, + objectUUID: "SomeObjectUUID_50", + }, + ]; + + dateNow1 = Date.now() - 35000; + dateNow2 = Date.now() - 30000; + dateNow3 = Date.now() - 25000; + dateNow4 = Date.now() - 20000; + dateNow5 = Date.now() - 15000; + + let counter = 0; + spyOn(CollectionsUtil, "dateNow").and.callFake(() => { + switch (++counter) { + case 1: + return dateNow1; + case 2: + return dateNow2; + case 3: + return dateNow3; + case 4: + return dateNow4; + case 5: + return dateNow5; + default: + return Date.now() + Math.ceil(Math.random() * 1000); + } }); + }); + + it("should verify filters Array of ErrorRecord correctly", () => { + // Given + const instance = new ErrorStoreImpl(); + instance.record(errorRecords[0]); + instance.record(errorRecords[1]); + instance.record(errorRecords[2]); + instance.record(errorRecords[3]); + instance.record(errorRecords[4]); + + // When + const filteredErrorRecords1 = filterErrorRecords(instance.records, [ + errorCodes[0], + errorCodes[4], + ]); + const filteredErrorRecords2 = filterErrorRecords( + instance.records, + [errorCodes[4]], + ["SomeCdRnd", null], + ); + const filteredErrorRecords3 = filterErrorRecords( + instance.records, + [], + ["SomeBng$", undefined], + ); + const filteredErrorRecords4 = filterErrorRecords( + instance.records, + null, + null, + ); + const filteredErrorRecords5 = filterErrorRecords(instance.records); + + // Then + expect(filteredErrorRecords1).toEqual([ + instance.records[4], + instance.records[0], + ]); + expect(filteredErrorRecords2).toEqual([ + instance.records[4], + instance.records[2], + instance.records[1], + ]); + expect(filteredErrorRecords3).toEqual([]); + expect(filteredErrorRecords4).toEqual([]); + expect(filteredErrorRecords5).toEqual([]); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.ts index 0744086992..ea863f859d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/error-store.impl.ts @@ -3,11 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpStatusCode } from '@angular/common/http'; +import { HttpStatusCode } from "@angular/common/http"; -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { ErrorRecord, ErrorStore, ErrorStoreChangeListener } from '../../../../common'; +import { + ErrorRecord, + ErrorStore, + ErrorStoreChangeListener, +} from "../../../../common"; /** * @inheritDoc @@ -15,343 +19,397 @@ import { ErrorRecord, ErrorStore, ErrorStoreChangeListener } from '../../../../c * ** ErrorStore implementation. */ export class ErrorStoreImpl implements ErrorStore { - /** - * ** Factory method. - */ - static of(errorRecords: ErrorRecord[]): ErrorStoreImpl { - return new ErrorStoreImpl(errorRecords); - } - - /** - * ** Factory method that returns empty store. - */ - static empty(): ErrorStoreImpl { - return ErrorStoreImpl.of(null); - } - - /** - * ** Creates ErrorStore from literal. - */ - static fromLiteral(errorRecords: ErrorRecord[]): ErrorStoreImpl { - return ErrorStoreImpl.of(ErrorStoreImpl.cloneDeepErrorRecords(errorRecords)); - } + /** + * ** Factory method. + */ + static of(errorRecords: ErrorRecord[]): ErrorStoreImpl { + return new ErrorStoreImpl(errorRecords); + } + + /** + * ** Factory method that returns empty store. + */ + static empty(): ErrorStoreImpl { + return ErrorStoreImpl.of(null); + } + + /** + * ** Creates ErrorStore from literal. + */ + static fromLiteral(errorRecords: ErrorRecord[]): ErrorStoreImpl { + return ErrorStoreImpl.of( + ErrorStoreImpl.cloneDeepErrorRecords(errorRecords), + ); + } + + /** + * ** Clone deep provided ErrorRecords. + */ + static cloneDeepErrorRecords(errorRecords: ErrorRecord[]): ErrorRecord[] { + return (errorRecords ?? []).map((r) => ({ ...r })); + } + + /** + * @inheritDoc + */ + records: ErrorRecord[]; + + /** + * @inheritDoc + */ + changeListeners: Array>; + + /** + * ** Constructor. + */ + constructor(errorRecords: ErrorRecord[] = []) { + this.records = errorRecords ?? []; + this.changeListeners = []; + } + + /** + * @inheritDoc + */ + hasErrors(): boolean { + return this.records.length > 0; + } + + /** + * @inheritDoc + */ + hasCode(...errorCodes: string[]): boolean { + return errorCodes.some( + (code) => this.records.findIndex((r) => r.code === code) !== -1, + ); + } + + /** + * @inheritDoc + */ + hasCodePattern(...errorCodesPatterns: string[]): boolean { + try { + return errorCodesPatterns.some((errorPattern) => { + if (!CollectionsUtil.isString(errorPattern)) { + return false; + } - /** - * ** Clone deep provided ErrorRecords. - */ - static cloneDeepErrorRecords(errorRecords: ErrorRecord[]): ErrorRecord[] { - return (errorRecords ?? []).map((r) => ({ ...r })); - } + const errorPatternRegex = new RegExp(errorPattern); - /** - * @inheritDoc - */ - records: ErrorRecord[]; - - /** - * @inheritDoc - */ - changeListeners: Array>; - - /** - * ** Constructor. - */ - constructor(errorRecords: ErrorRecord[] = []) { - this.records = errorRecords ?? []; - this.changeListeners = []; - } + return ( + this.records.findIndex((r) => errorPatternRegex.test(r.code)) !== -1 + ); + }); + } catch (e) { + console.error(e); - /** - * @inheritDoc - */ - hasErrors(): boolean { - return this.records.length > 0; + return false; } - - /** - * @inheritDoc - */ - hasCode(...errorCodes: string[]): boolean { - return errorCodes.some((code) => this.records.findIndex((r) => r.code === code) !== -1); + } + + /** + * @inheritDoc + */ + record( + errorCode: string, + objectUUID: string, + error: Error, + httpStatusCode?: HttpStatusCode, + ): void; + /** + * @inheritDoc + */ + record(errorRecord: ErrorRecord): void; + record( + param: string | ErrorRecord, + objectUUID?: string, + error?: Error, + httpStatusCode?: HttpStatusCode, + ): void { + const errorRecordsShallowCopy = [...this.records]; + + if ( + CollectionsUtil.isString(param) && + CollectionsUtil.isString(objectUUID) + ) { + const foundIndex = errorRecordsShallowCopy.findIndex( + (r) => r.code === param && r.objectUUID === objectUUID, + ); + const errorRecord: ErrorRecord = { + code: param, + objectUUID, + time: CollectionsUtil.dateNow(), + error: error ?? null, + httpStatusCode: httpStatusCode ?? null, + }; + + if (foundIndex === -1) { + errorRecordsShallowCopy.push(errorRecord); + } else { + errorRecordsShallowCopy.splice(foundIndex, 1, errorRecord); + } + } else if ( + CollectionsUtil.isLiteralObject(param) && + CollectionsUtil.isNil(objectUUID) + ) { + const foundIndex = errorRecordsShallowCopy.findIndex( + (r) => + r.code === (param as ErrorRecord).code && + r.objectUUID === (param as ErrorRecord).objectUUID, + ); + const errorRecord: ErrorRecord = { + ...(param as ErrorRecord), + time: CollectionsUtil.dateNow(), + }; + + if (foundIndex === -1) { + errorRecordsShallowCopy.push(errorRecord); + } else { + errorRecordsShallowCopy.splice(foundIndex, 1, errorRecord); + } } - /** - * @inheritDoc - */ - hasCodePattern(...errorCodesPatterns: string[]): boolean { - try { - return errorCodesPatterns.some((errorPattern) => { - if (!CollectionsUtil.isString(errorPattern)) { - return false; - } + this.records = errorRecordsShallowCopy; - const errorPatternRegex = new RegExp(errorPattern); + ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); + } - return this.records.findIndex((r) => errorPatternRegex.test(r.code)) !== -1; - }); - } catch (e) { - console.error(e); + /** + * @inheritDoc + */ + removeCode(...errorCodes: string[]): void { + let errorRecordsShallowCopy = [...this.records]; - return false; - } + try { + errorRecordsShallowCopy = errorRecordsShallowCopy.filter( + (r) => !errorCodes.includes(r.code), + ); + } catch (e) { + console.error(e); } - /** - * @inheritDoc - */ - record(errorCode: string, objectUUID: string, error: Error, httpStatusCode?: HttpStatusCode): void; - /** - * @inheritDoc - */ - record(errorRecord: ErrorRecord): void; - record(param: string | ErrorRecord, objectUUID?: string, error?: Error, httpStatusCode?: HttpStatusCode): void { - const errorRecordsShallowCopy = [...this.records]; - - if (CollectionsUtil.isString(param) && CollectionsUtil.isString(objectUUID)) { - const foundIndex = errorRecordsShallowCopy.findIndex((r) => r.code === param && r.objectUUID === objectUUID); - const errorRecord: ErrorRecord = { - code: param, - objectUUID, - time: CollectionsUtil.dateNow(), - error: error ?? null, - httpStatusCode: httpStatusCode ?? null - }; - - if (foundIndex === -1) { - errorRecordsShallowCopy.push(errorRecord); - } else { - errorRecordsShallowCopy.splice(foundIndex, 1, errorRecord); - } - } else if (CollectionsUtil.isLiteralObject(param) && CollectionsUtil.isNil(objectUUID)) { - const foundIndex = errorRecordsShallowCopy.findIndex( - (r) => r.code === (param as ErrorRecord).code && r.objectUUID === (param as ErrorRecord).objectUUID - ); - const errorRecord: ErrorRecord = { - ...(param as ErrorRecord), - time: CollectionsUtil.dateNow() - }; - - if (foundIndex === -1) { - errorRecordsShallowCopy.push(errorRecord); - } else { - errorRecordsShallowCopy.splice(foundIndex, 1, errorRecord); - } - } - - this.records = errorRecordsShallowCopy; + this.records = errorRecordsShallowCopy; - ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); - } + ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); + } - /** - * @inheritDoc - */ - removeCode(...errorCodes: string[]): void { - let errorRecordsShallowCopy = [...this.records]; + /** + * @inheritDoc + */ + removeCodePattern(...errorCodePatterns: string[]): void { + let errorCodesShallowCopy = [...this.records]; - try { - errorRecordsShallowCopy = errorRecordsShallowCopy.filter((r) => !errorCodes.includes(r.code)); - } catch (e) { - console.error(e); + for (const errorPattern of errorCodePatterns) { + try { + if (!CollectionsUtil.isString(errorPattern)) { + continue; } - this.records = errorRecordsShallowCopy; + const errorPatternRegex = new RegExp(errorPattern); - ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); + errorCodesShallowCopy = errorCodesShallowCopy.filter( + (r) => !errorPatternRegex.test(r.code), + ); + } catch (e) { + console.error(e); + } } - /** - * @inheritDoc - */ - removeCodePattern(...errorCodePatterns: string[]): void { - let errorCodesShallowCopy = [...this.records]; + this.records = errorCodesShallowCopy; - for (const errorPattern of errorCodePatterns) { - try { - if (!CollectionsUtil.isString(errorPattern)) { - continue; - } + ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); + } - const errorPatternRegex = new RegExp(errorPattern); + /** + * @inheritDoc + */ + findRecords(...errorCodes: string[]): ErrorRecord[] { + return this.records.filter((r) => errorCodes.includes(r.code)); + } - errorCodesShallowCopy = errorCodesShallowCopy.filter((r) => !errorPatternRegex.test(r.code)); - } catch (e) { - console.error(e); - } + /** + * @inheritDoc + */ + findRecordsByPattern(...errorCodePatterns: string[]): ErrorRecord[] { + let foundRecords: ErrorRecord[] = []; + + for (const errorPattern of errorCodePatterns) { + try { + if (!CollectionsUtil.isString(errorPattern)) { + continue; } - this.records = errorCodesShallowCopy; + const errorPatternRegex = new RegExp(errorPattern); - ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); - } + const filteredRecords = this.records.filter((r) => + errorPatternRegex.test(r.code), + ); - /** - * @inheritDoc - */ - findRecords(...errorCodes: string[]): ErrorRecord[] { - return this.records.filter((r) => errorCodes.includes(r.code)); + foundRecords = foundRecords.concat(...filteredRecords); + } catch (e) { + console.error(e); + } } - /** - * @inheritDoc - */ - findRecordsByPattern(...errorCodePatterns: string[]): ErrorRecord[] { - let foundRecords: ErrorRecord[] = []; - - for (const errorPattern of errorCodePatterns) { - try { - if (!CollectionsUtil.isString(errorPattern)) { - continue; - } - - const errorPatternRegex = new RegExp(errorPattern); - - const filteredRecords = this.records.filter((r) => errorPatternRegex.test(r.code)); - - foundRecords = foundRecords.concat(...filteredRecords); - } catch (e) { - console.error(e); - } - } - - return foundRecords; + return foundRecords; + } + + /** + * @inheritDoc + */ + distinctErrorRecords(errorRecords: ErrorRecord[]): ErrorRecord[] { + const _errorRecords: ErrorRecord[] = CollectionsUtil.isArray(errorRecords) + ? errorRecords + : []; + + return this.records.filter( + (r) => + _errorRecords.findIndex((rInjected) => + ErrorStoreImpl._checkErrorRecordsEquality(r, rInjected), + ) === -1, + ); + } + + /** + * @inheritDoc + */ + purge(injectedStore: ErrorStoreImpl): void { + if (!(injectedStore instanceof ErrorStoreImpl)) { + return; } - /** - * @inheritDoc - */ - distinctErrorRecords(errorRecords: ErrorRecord[]): ErrorRecord[] { - const _errorRecords: ErrorRecord[] = CollectionsUtil.isArray(errorRecords) ? errorRecords : []; - - return this.records.filter( - (r) => _errorRecords.findIndex((rInjected) => ErrorStoreImpl._checkErrorRecordsEquality(r, rInjected)) === -1 - ); + if (this.equals(injectedStore)) { + return; } - /** - * @inheritDoc - */ - purge(injectedStore: ErrorStoreImpl): void { - if (!(injectedStore instanceof ErrorStoreImpl)) { - return; - } + this.records = [...injectedStore.records]; - if (this.equals(injectedStore)) { - return; - } + ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); + } - this.records = [...injectedStore.records]; - - ErrorStoreImpl._executeChangeListeners(this, this.changeListeners); + /** + * @inheritDoc + */ + onChange(callback: (store: this) => void): void { + if (CollectionsUtil.isFunction(callback)) { + this.changeListeners.push(callback); } + } - /** - * @inheritDoc - */ - onChange(callback: (store: this) => void): void { - if (CollectionsUtil.isFunction(callback)) { - this.changeListeners.push(callback); - } - } + /** + * @inheritDoc + */ + dispose(): void { + this.clear(); - /** - * @inheritDoc - */ - dispose(): void { - this.clear(); + this.changeListeners = []; + } - this.changeListeners = []; + /** + * @inheritDoc + */ + clear(): void { + try { + this.records.length = 0; + this.records = []; + } catch (e) { + console.error(e); } - - /** - * @inheritDoc - */ - clear(): void { - try { - this.records.length = 0; - this.records = []; - } catch (e) { - console.error(e); - } + } + + /** + * @inheritDoc + */ + toLiteral(): ErrorRecord[] { + return [...this.records]; + } + + /** + * @inheritDoc + */ + toLiteralCloneDeep(): ErrorRecord[] { + return this.records.map((r) => { + return { ...r }; + }); + } + + /** + * @inheritDoc + */ + copy(): ErrorStoreImpl { + return ErrorStoreImpl.of([...this.records]); + } + + /** + * @inheritDoc + */ + equals(obj: ErrorStore): boolean { + if (!(obj instanceof ErrorStoreImpl)) { + return false; } - /** - * @inheritDoc - */ - toLiteral(): ErrorRecord[] { - return [...this.records]; + if (this.records.length !== obj.records.length) { + return false; } - /** - * @inheritDoc - */ - toLiteralCloneDeep(): ErrorRecord[] { - return this.records.map((r) => { - return { ...r }; - }); + for (let i = 0; i < this.records.length; i++) { + if ( + !ErrorStoreImpl._checkErrorRecordsEquality( + this.records[i], + obj.records[i], + ) + ) { + return false; + } } - /** - * @inheritDoc - */ - copy(): ErrorStoreImpl { - return ErrorStoreImpl.of([...this.records]); - } + return true; + } - /** - * @inheritDoc - */ - equals(obj: ErrorStore): boolean { - if (!(obj instanceof ErrorStoreImpl)) { - return false; - } - - if (this.records.length !== obj.records.length) { - return false; - } - - for (let i = 0; i < this.records.length; i++) { - if (!ErrorStoreImpl._checkErrorRecordsEquality(this.records[i], obj.records[i])) { - return false; - } - } - - return true; + private static _checkErrorRecordsEquality( + errorRecord1: ErrorRecord, + errorRecord2: ErrorRecord, + ): boolean { + if (errorRecord1.code !== errorRecord2.code) { + return false; } - private static _checkErrorRecordsEquality(errorRecord1: ErrorRecord, errorRecord2: ErrorRecord): boolean { - if (errorRecord1.code !== errorRecord2.code) { - return false; - } - - if (errorRecord1.objectUUID !== errorRecord2.objectUUID) { - return false; - } - - if (errorRecord1.time !== errorRecord2.time) { - return false; - } + if (errorRecord1.objectUUID !== errorRecord2.objectUUID) { + return false; + } - if (errorRecord1.httpStatusCode !== errorRecord2.httpStatusCode) { - return false; - } + if (errorRecord1.time !== errorRecord2.time) { + return false; + } - return errorRecord1.error === errorRecord2.error; + if (errorRecord1.httpStatusCode !== errorRecord2.httpStatusCode) { + return false; } - private static _executeChangeListeners(store: ErrorStoreImpl, changeListeners: Array>): void { - if (!CollectionsUtil.isArray(changeListeners) || changeListeners.length === 0) { - return; - } + return errorRecord1.error === errorRecord2.error; + } + + private static _executeChangeListeners( + store: ErrorStoreImpl, + changeListeners: Array>, + ): void { + if ( + !CollectionsUtil.isArray(changeListeners) || + changeListeners.length === 0 + ) { + return; + } - for (const listener of changeListeners) { - try { - listener(store); - } catch (e) { - console.error(`Taurus ErrorStore failed to execute change listeners`, e); - } - } + for (const listener of changeListeners) { + try { + listener(store); + } catch (e) { + console.error( + `Taurus ErrorStore failed to execute change listeners`, + e, + ); + } } + } } /** @@ -359,27 +417,32 @@ export class ErrorStoreImpl implements ErrorStore { * @protected */ export const filterErrorRecords = ( - errorRecords: ErrorRecord[], - errorCodes: string[] = [], - errorCodesPatterns: string[] = [] + errorRecords: ErrorRecord[], + errorCodes: string[] = [], + errorCodesPatterns: string[] = [], ): ErrorRecord[] => { - const errorPatternsRegex: RegExp[] = []; - - try { - (errorCodesPatterns ?? []).forEach((errorPattern) => { - if (!CollectionsUtil.isString(errorPattern)) { - return; - } - - errorPatternsRegex.push(new RegExp(errorPattern)); - }); - } catch (e) { - console.error(e); - } - - return [...errorRecords] - .sort((r1, r2) => r2.time - r1.time) - .filter((r) => { - return (errorCodes ?? []).includes(r.code) || errorPatternsRegex.some((errorPatternRegex) => errorPatternRegex.test(r.code)); - }); + const errorPatternsRegex: RegExp[] = []; + + try { + (errorCodesPatterns ?? []).forEach((errorPattern) => { + if (!CollectionsUtil.isString(errorPattern)) { + return; + } + + errorPatternsRegex.push(new RegExp(errorPattern)); + }); + } catch (e) { + console.error(e); + } + + return [...errorRecords] + .sort((r1, r2) => r2.time - r1.time) + .filter((r) => { + return ( + (errorCodes ?? []).includes(r.code) || + errorPatternsRegex.some((errorPatternRegex) => + errorPatternRegex.test(r.code), + ) + ); + }); }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/index.ts index 8a528eddfb..6aa9a54589 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/store/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './error-store.impl'; +export * from "./error-store.impl"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.spec.ts index d5e7e2cb84..99d1267113 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.spec.ts @@ -7,476 +7,565 @@ @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access */ -import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; +import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { ApiErrorMessage, ErrorRecord, ServiceHttpErrorCodes } from '../../../common'; +import { + ApiErrorMessage, + ErrorRecord, + ServiceHttpErrorCodes, +} from "../../../common"; -import { getApiFormattedErrorMessage, getHumanReadableStatusText, processServiceRequestError } from './error.utils'; +import { + getApiFormattedErrorMessage, + getHumanReadableStatusText, + processServiceRequestError, +} from "./error.utils"; -describe('processServiceRequestError', () => { - describe('should verify will return ErrorRecord for potential code bugs, when', () => { - const error = new Error('Random Error'); - const syntaxError = new SyntaxError('Unsupported action'); - const params: Array<[string, string, Record, unknown, ErrorRecord, number]> = [ - [ - 'objectUUID is null, ServiceHttpErrorCodes is null and error is literal object', - null, - null, - {}, - { - code: 'UnknownClassName_UnknownPublicName_UnknownMethodName_Generic', - error: null, - objectUUID: 'ErrorCodeClass_uuid' - }, - 1 - ], - [ - 'objectUUID is provided, ServiceHttpErrorCodes is undefined and error is Error provided', - 'ErrorCodeClass_uuid_111', - undefined, - error, - { - code: 'UnknownClassName_UnknownPublicName_UnknownMethodName_Generic', - error, - objectUUID: 'ErrorCodeClass_uuid_111' - }, - 0 - ], - [ - 'objectUUID is undefined, ServiceHttpErrorCodes is Map and error is Array', - undefined, - new Map() as any, - [], - { - code: 'UnknownClassName_UnknownPublicName_UnknownMethodName_Generic', - error: null, - objectUUID: 'ErrorCodeClass_uuid' - }, - 1 - ], - [ - 'objectUUID is provided, ServiceHttpErrorCodes is Array and error is SyntaxError provided', - 'ErrorCodeClass_uuid_222', - [] as any, - syntaxError, - { - code: 'UnknownClassName_UnknownPublicName_UnknownMethodName_Generic', - error: syntaxError, - objectUUID: 'ErrorCodeClass_uuid_222' - }, - 0 - ] - ]; +describe("processServiceRequestError", () => { + describe("should verify will return ErrorRecord for potential code bugs, when", () => { + const error = new Error("Random Error"); + const syntaxError = new SyntaxError("Unsupported action"); + const params: Array< + [ + string, + string, + Record, + unknown, + ErrorRecord, + number, + ] + > = [ + [ + "objectUUID is null, ServiceHttpErrorCodes is null and error is literal object", + null, + null, + {}, + { + code: "UnknownClassName_UnknownPublicName_UnknownMethodName_Generic", + error: null, + objectUUID: "ErrorCodeClass_uuid", + }, + 1, + ], + [ + "objectUUID is provided, ServiceHttpErrorCodes is undefined and error is Error provided", + "ErrorCodeClass_uuid_111", + undefined, + error, + { + code: "UnknownClassName_UnknownPublicName_UnknownMethodName_Generic", + error, + objectUUID: "ErrorCodeClass_uuid_111", + }, + 0, + ], + [ + "objectUUID is undefined, ServiceHttpErrorCodes is Map and error is Array", + undefined, + new Map() as any, + [], + { + code: "UnknownClassName_UnknownPublicName_UnknownMethodName_Generic", + error: null, + objectUUID: "ErrorCodeClass_uuid", + }, + 1, + ], + [ + "objectUUID is provided, ServiceHttpErrorCodes is Array and error is SyntaxError provided", + "ErrorCodeClass_uuid_222", + [] as any, + syntaxError, + { + code: "UnknownClassName_UnknownPublicName_UnknownMethodName_Generic", + error: syntaxError, + objectUUID: "ErrorCodeClass_uuid_222", + }, + 0, + ], + ]; - for (const [description, objectUUID, serviceHttpErrorCodes, _error, assertion, numberOfInvoke] of params) { - it(`${description}`, () => { - // Given - const spyGenerateObjectUUID = spyOn(CollectionsUtil, 'generateObjectUUID').and.returnValue('ErrorCodeClass_uuid'); + for (const [ + description, + objectUUID, + serviceHttpErrorCodes, + _error, + assertion, + numberOfInvoke, + ] of params) { + it(`${description}`, () => { + // Given + const spyGenerateObjectUUID = spyOn( + CollectionsUtil, + "generateObjectUUID", + ).and.returnValue("ErrorCodeClass_uuid"); - // When - const errorRecord = processServiceRequestError(objectUUID, serviceHttpErrorCodes, _error); + // When + const errorRecord = processServiceRequestError( + objectUUID, + serviceHttpErrorCodes, + _error, + ); - // Then - expect(errorRecord).toEqual(assertion); + // Then + expect(errorRecord).toEqual(assertion); - expect(spyGenerateObjectUUID).toHaveBeenCalledTimes(numberOfInvoke); - if (numberOfInvoke > 0) { - expect(spyGenerateObjectUUID).toHaveBeenCalledWith('UnknownClassName'); - } - }); + expect(spyGenerateObjectUUID).toHaveBeenCalledTimes(numberOfInvoke); + if (numberOfInvoke > 0) { + expect(spyGenerateObjectUUID).toHaveBeenCalledWith( + "UnknownClassName", + ); } - }); + }); + } + }); - describe('should verify will return ErrorRecord for usual scenarios when errorCode is', () => { - const serviceHttpErrorCodes: Record = { - All: 'ErrorCodeClass_SpiedPublicName_andMethod_', - ClientErrors: 'ErrorCodeClass_SpiedPublicName_andMethod_4\\d\\d', - BadRequest: 'ErrorCodeClass_SpiedPublicName_andMethod_400', - Unauthorized: 'ErrorCodeClass_SpiedPublicName_andMethod_401', - Forbidden: 'ErrorCodeClass_SpiedPublicName_andMethod_403', - NotFound: 'ErrorCodeClass_SpiedPublicName_andMethod_404', - MethodNotAllowed: 'ErrorCodeClass_SpiedPublicName_andMethod_405', - Conflict: 'ErrorCodeClass_SpiedPublicName_andMethod_409', - UnprocessableEntity: 'ErrorCodeClass_SpiedPublicName_andMethod_422', - ServerErrors: 'ErrorCodeClass_SpiedPublicName_andMethod_5\\d\\d', - InternalServerError: 'ErrorCodeClass_SpiedPublicName_andMethod_500', - ServiceUnavailable: 'ErrorCodeClass_SpiedPublicName_andMethod_503', - Unknown: 'ErrorCodeClass_SpiedPublicName_andMethod_unknown' - }; - const errors: Partial> = { - BadRequest: new HttpErrorResponse({ - status: HttpStatusCode.BadRequest, - error: new Error(`${HttpStatusCode.BadRequest}`) - }), - Unauthorized: new HttpErrorResponse({ - status: HttpStatusCode.Unauthorized, - error: new Error(`${HttpStatusCode.Unauthorized}`) - }), - Forbidden: new HttpErrorResponse({ - status: HttpStatusCode.Forbidden, - error: new Error(`${HttpStatusCode.Forbidden}`) - }), - NotFound: new HttpErrorResponse({ - status: HttpStatusCode.NotFound, - error: new Error(`${HttpStatusCode.NotFound}`) - }), - MethodNotAllowed: new HttpErrorResponse({ - status: HttpStatusCode.MethodNotAllowed, - error: new Error(`${HttpStatusCode.MethodNotAllowed}`) - }), - Conflict: new HttpErrorResponse({ - status: HttpStatusCode.Conflict, - error: new Error(`${HttpStatusCode.Conflict}`) - }), - UnprocessableEntity: new HttpErrorResponse({ - status: HttpStatusCode.UnprocessableEntity, - error: new Error(`${HttpStatusCode.UnprocessableEntity}`) - }), - InternalServerError: new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: new Error(`${HttpStatusCode.InternalServerError}`) - }), - ServiceUnavailable: new HttpErrorResponse({ - status: HttpStatusCode.ServiceUnavailable, - error: new Error(`${HttpStatusCode.ServiceUnavailable}`) - }), - Unknown: new HttpErrorResponse({ - status: HttpStatusCode.BadGateway, - error: new Error(`${HttpStatusCode.BadGateway}`) - }) - }; - const params: Array<[string, string, Record, HttpErrorResponse, ErrorRecord, number]> = [ - [ - `"${serviceHttpErrorCodes.BadRequest}" for HttpStatusCode "${HttpStatusCode.BadRequest}"`, - null, - serviceHttpErrorCodes, - errors.BadRequest, - { - code: serviceHttpErrorCodes.BadRequest, - error: errors.BadRequest, - objectUUID: 'ErrorCodeClass_uuid', - httpStatusCode: HttpStatusCode.BadRequest - }, - 1 - ], - [ - `"${serviceHttpErrorCodes.Unauthorized}" for HttpStatusCode "${HttpStatusCode.Unauthorized}"`, - 'ErrorCodeClass_uuid_111', - serviceHttpErrorCodes, - errors.Unauthorized, - { - code: serviceHttpErrorCodes.Unauthorized, - error: errors.Unauthorized, - objectUUID: 'ErrorCodeClass_uuid_111', - httpStatusCode: HttpStatusCode.Unauthorized - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.Forbidden}" for HttpStatusCode "${HttpStatusCode.Forbidden}"`, - 'ErrorCodeClass_uuid_112', - serviceHttpErrorCodes, - errors.Forbidden, - { - code: serviceHttpErrorCodes.Forbidden, - error: errors.Forbidden, - objectUUID: 'ErrorCodeClass_uuid_112', - httpStatusCode: HttpStatusCode.Forbidden - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.NotFound}" for HttpStatusCode "${HttpStatusCode.NotFound}"`, - undefined, - serviceHttpErrorCodes, - errors.NotFound, - { - code: serviceHttpErrorCodes.NotFound, - error: errors.NotFound, - objectUUID: 'ErrorCodeClass_uuid', - httpStatusCode: HttpStatusCode.NotFound - }, - 1 - ], - [ - `"${serviceHttpErrorCodes.MethodNotAllowed}" for HttpStatusCode "${HttpStatusCode.MethodNotAllowed}"`, - 'ErrorCodeClass_uuid_113', - serviceHttpErrorCodes, - errors.MethodNotAllowed, - { - code: serviceHttpErrorCodes.MethodNotAllowed, - error: errors.MethodNotAllowed, - objectUUID: 'ErrorCodeClass_uuid_113', - httpStatusCode: HttpStatusCode.MethodNotAllowed - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.Conflict}" for HttpStatusCode "${HttpStatusCode.Conflict}"`, - 'ErrorCodeClass_uuid_114', - serviceHttpErrorCodes, - errors.Conflict, - { - code: serviceHttpErrorCodes.Conflict, - error: errors.Conflict, - objectUUID: 'ErrorCodeClass_uuid_114', - httpStatusCode: HttpStatusCode.Conflict - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.UnprocessableEntity}" for HttpStatusCode "${HttpStatusCode.UnprocessableEntity}"`, - 'ErrorCodeClass_uuid_115', - serviceHttpErrorCodes, - errors.UnprocessableEntity, - { - code: serviceHttpErrorCodes.UnprocessableEntity, - error: errors.UnprocessableEntity, - objectUUID: 'ErrorCodeClass_uuid_115', - httpStatusCode: HttpStatusCode.UnprocessableEntity - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.InternalServerError}" for HttpStatusCode "${HttpStatusCode.InternalServerError}"`, - 'ErrorCodeClass_uuid_116', - serviceHttpErrorCodes, - errors.InternalServerError, - { - code: serviceHttpErrorCodes.InternalServerError, - error: errors.InternalServerError, - objectUUID: 'ErrorCodeClass_uuid_116', - httpStatusCode: HttpStatusCode.InternalServerError - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.ServiceUnavailable}" for HttpStatusCode "${HttpStatusCode.ServiceUnavailable}"`, - 'ErrorCodeClass_uuid_117', - serviceHttpErrorCodes, - errors.ServiceUnavailable, - { - code: serviceHttpErrorCodes.ServiceUnavailable, - error: errors.ServiceUnavailable, - objectUUID: 'ErrorCodeClass_uuid_117', - httpStatusCode: HttpStatusCode.ServiceUnavailable - }, - 0 - ], - [ - `"${serviceHttpErrorCodes.Unknown}" for HttpStatusCode "${HttpStatusCode.BadGateway}"`, - 'ErrorCodeClass_uuid_118', - serviceHttpErrorCodes, - errors.Unknown, - { - code: serviceHttpErrorCodes.Unknown, - error: errors.Unknown, - objectUUID: 'ErrorCodeClass_uuid_118', - httpStatusCode: HttpStatusCode.BadGateway - }, - 0 - ] - ]; + describe("should verify will return ErrorRecord for usual scenarios when errorCode is", () => { + const serviceHttpErrorCodes: Record = { + All: "ErrorCodeClass_SpiedPublicName_andMethod_", + ClientErrors: "ErrorCodeClass_SpiedPublicName_andMethod_4\\d\\d", + BadRequest: "ErrorCodeClass_SpiedPublicName_andMethod_400", + Unauthorized: "ErrorCodeClass_SpiedPublicName_andMethod_401", + Forbidden: "ErrorCodeClass_SpiedPublicName_andMethod_403", + NotFound: "ErrorCodeClass_SpiedPublicName_andMethod_404", + MethodNotAllowed: "ErrorCodeClass_SpiedPublicName_andMethod_405", + Conflict: "ErrorCodeClass_SpiedPublicName_andMethod_409", + UnprocessableEntity: "ErrorCodeClass_SpiedPublicName_andMethod_422", + ServerErrors: "ErrorCodeClass_SpiedPublicName_andMethod_5\\d\\d", + InternalServerError: "ErrorCodeClass_SpiedPublicName_andMethod_500", + ServiceUnavailable: "ErrorCodeClass_SpiedPublicName_andMethod_503", + Unknown: "ErrorCodeClass_SpiedPublicName_andMethod_unknown", + }; + const errors: Partial< + Record + > = { + BadRequest: new HttpErrorResponse({ + status: HttpStatusCode.BadRequest, + error: new Error(`${HttpStatusCode.BadRequest}`), + }), + Unauthorized: new HttpErrorResponse({ + status: HttpStatusCode.Unauthorized, + error: new Error(`${HttpStatusCode.Unauthorized}`), + }), + Forbidden: new HttpErrorResponse({ + status: HttpStatusCode.Forbidden, + error: new Error(`${HttpStatusCode.Forbidden}`), + }), + NotFound: new HttpErrorResponse({ + status: HttpStatusCode.NotFound, + error: new Error(`${HttpStatusCode.NotFound}`), + }), + MethodNotAllowed: new HttpErrorResponse({ + status: HttpStatusCode.MethodNotAllowed, + error: new Error(`${HttpStatusCode.MethodNotAllowed}`), + }), + Conflict: new HttpErrorResponse({ + status: HttpStatusCode.Conflict, + error: new Error(`${HttpStatusCode.Conflict}`), + }), + UnprocessableEntity: new HttpErrorResponse({ + status: HttpStatusCode.UnprocessableEntity, + error: new Error(`${HttpStatusCode.UnprocessableEntity}`), + }), + InternalServerError: new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: new Error(`${HttpStatusCode.InternalServerError}`), + }), + ServiceUnavailable: new HttpErrorResponse({ + status: HttpStatusCode.ServiceUnavailable, + error: new Error(`${HttpStatusCode.ServiceUnavailable}`), + }), + Unknown: new HttpErrorResponse({ + status: HttpStatusCode.BadGateway, + error: new Error(`${HttpStatusCode.BadGateway}`), + }), + }; + const params: Array< + [ + string, + string, + Record, + HttpErrorResponse, + ErrorRecord, + number, + ] + > = [ + [ + `"${serviceHttpErrorCodes.BadRequest}" for HttpStatusCode "${HttpStatusCode.BadRequest}"`, + null, + serviceHttpErrorCodes, + errors.BadRequest, + { + code: serviceHttpErrorCodes.BadRequest, + error: errors.BadRequest, + objectUUID: "ErrorCodeClass_uuid", + httpStatusCode: HttpStatusCode.BadRequest, + }, + 1, + ], + [ + `"${serviceHttpErrorCodes.Unauthorized}" for HttpStatusCode "${HttpStatusCode.Unauthorized}"`, + "ErrorCodeClass_uuid_111", + serviceHttpErrorCodes, + errors.Unauthorized, + { + code: serviceHttpErrorCodes.Unauthorized, + error: errors.Unauthorized, + objectUUID: "ErrorCodeClass_uuid_111", + httpStatusCode: HttpStatusCode.Unauthorized, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.Forbidden}" for HttpStatusCode "${HttpStatusCode.Forbidden}"`, + "ErrorCodeClass_uuid_112", + serviceHttpErrorCodes, + errors.Forbidden, + { + code: serviceHttpErrorCodes.Forbidden, + error: errors.Forbidden, + objectUUID: "ErrorCodeClass_uuid_112", + httpStatusCode: HttpStatusCode.Forbidden, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.NotFound}" for HttpStatusCode "${HttpStatusCode.NotFound}"`, + undefined, + serviceHttpErrorCodes, + errors.NotFound, + { + code: serviceHttpErrorCodes.NotFound, + error: errors.NotFound, + objectUUID: "ErrorCodeClass_uuid", + httpStatusCode: HttpStatusCode.NotFound, + }, + 1, + ], + [ + `"${serviceHttpErrorCodes.MethodNotAllowed}" for HttpStatusCode "${HttpStatusCode.MethodNotAllowed}"`, + "ErrorCodeClass_uuid_113", + serviceHttpErrorCodes, + errors.MethodNotAllowed, + { + code: serviceHttpErrorCodes.MethodNotAllowed, + error: errors.MethodNotAllowed, + objectUUID: "ErrorCodeClass_uuid_113", + httpStatusCode: HttpStatusCode.MethodNotAllowed, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.Conflict}" for HttpStatusCode "${HttpStatusCode.Conflict}"`, + "ErrorCodeClass_uuid_114", + serviceHttpErrorCodes, + errors.Conflict, + { + code: serviceHttpErrorCodes.Conflict, + error: errors.Conflict, + objectUUID: "ErrorCodeClass_uuid_114", + httpStatusCode: HttpStatusCode.Conflict, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.UnprocessableEntity}" for HttpStatusCode "${HttpStatusCode.UnprocessableEntity}"`, + "ErrorCodeClass_uuid_115", + serviceHttpErrorCodes, + errors.UnprocessableEntity, + { + code: serviceHttpErrorCodes.UnprocessableEntity, + error: errors.UnprocessableEntity, + objectUUID: "ErrorCodeClass_uuid_115", + httpStatusCode: HttpStatusCode.UnprocessableEntity, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.InternalServerError}" for HttpStatusCode "${HttpStatusCode.InternalServerError}"`, + "ErrorCodeClass_uuid_116", + serviceHttpErrorCodes, + errors.InternalServerError, + { + code: serviceHttpErrorCodes.InternalServerError, + error: errors.InternalServerError, + objectUUID: "ErrorCodeClass_uuid_116", + httpStatusCode: HttpStatusCode.InternalServerError, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.ServiceUnavailable}" for HttpStatusCode "${HttpStatusCode.ServiceUnavailable}"`, + "ErrorCodeClass_uuid_117", + serviceHttpErrorCodes, + errors.ServiceUnavailable, + { + code: serviceHttpErrorCodes.ServiceUnavailable, + error: errors.ServiceUnavailable, + objectUUID: "ErrorCodeClass_uuid_117", + httpStatusCode: HttpStatusCode.ServiceUnavailable, + }, + 0, + ], + [ + `"${serviceHttpErrorCodes.Unknown}" for HttpStatusCode "${HttpStatusCode.BadGateway}"`, + "ErrorCodeClass_uuid_118", + serviceHttpErrorCodes, + errors.Unknown, + { + code: serviceHttpErrorCodes.Unknown, + error: errors.Unknown, + objectUUID: "ErrorCodeClass_uuid_118", + httpStatusCode: HttpStatusCode.BadGateway, + }, + 0, + ], + ]; - for (const [_description, _objectUUID, _serviceHttpErrorCodes, _error, _assertion, _numberOfInvoke] of params) { - it(`${_description}`, () => { - // Given - const spyGenerateObjectUUID = spyOn(CollectionsUtil, 'generateObjectUUID').and.returnValue('ErrorCodeClass_uuid'); + for (const [ + _description, + _objectUUID, + _serviceHttpErrorCodes, + _error, + _assertion, + _numberOfInvoke, + ] of params) { + it(`${_description}`, () => { + // Given + const spyGenerateObjectUUID = spyOn( + CollectionsUtil, + "generateObjectUUID", + ).and.returnValue("ErrorCodeClass_uuid"); - // When - const _errorRecord = processServiceRequestError(_objectUUID, _serviceHttpErrorCodes, _error); + // When + const _errorRecord = processServiceRequestError( + _objectUUID, + _serviceHttpErrorCodes, + _error, + ); - // Then - expect(_errorRecord).toEqual(_assertion); - expect(spyGenerateObjectUUID).toHaveBeenCalledTimes(_numberOfInvoke); - if (_numberOfInvoke > 0) { - expect(spyGenerateObjectUUID).toHaveBeenCalledWith('UnknownClassName'); - } - }); + // Then + expect(_errorRecord).toEqual(_assertion); + expect(spyGenerateObjectUUID).toHaveBeenCalledTimes(_numberOfInvoke); + if (_numberOfInvoke > 0) { + expect(spyGenerateObjectUUID).toHaveBeenCalledWith( + "UnknownClassName", + ); } - }); + }); + } + }); - describe('should verify will return ErrorRecord for various cases when serviceHttpErrorRecords are provided and error is not HttpErrorResponse', () => { - // Given - const serviceHttpErrorCodes: Record = { - All: 'ErrorCodeClass_SpiedPublicName_andMethod_', - ClientErrors: 'ErrorCodeClass_SpiedPublicName_andMethod_4\\d\\d', - BadRequest: 'ErrorCodeClass_SpiedPublicName_andMethod_400', - Unauthorized: 'ErrorCodeClass_SpiedPublicName_andMethod_401', - Forbidden: 'ErrorCodeClass_SpiedPublicName_andMethod_403', - NotFound: 'ErrorCodeClass_SpiedPublicName_andMethod_404', - MethodNotAllowed: 'ErrorCodeClass_SpiedPublicName_andMethod_405', - Conflict: 'ErrorCodeClass_SpiedPublicName_andMethod_409', - UnprocessableEntity: 'ErrorCodeClass_SpiedPublicName_andMethod_422', - ServerErrors: 'ErrorCodeClass_SpiedPublicName_andMethod_5\\d\\d', - InternalServerError: 'ErrorCodeClass_SpiedPublicName_andMethod_500', - ServiceUnavailable: 'ErrorCodeClass_SpiedPublicName_andMethod_503', - Unknown: 'ErrorCodeClass_SpiedPublicName_andMethod_unknown' - }; - const errors: unknown[] = [new Error(`Random Error 1`), null, new Error(`Random Error 3`)]; - const params: Array<[string, string, Record, unknown, ErrorRecord, number]> = [ - [ - `error is instanceof Error`, - null, - serviceHttpErrorCodes, - errors[0], - { - code: serviceHttpErrorCodes.Unknown, - error: errors[0] as any, - objectUUID: 'ErrorCodeClass_uuid' - }, - 1 - ], - [ - `error is null`, - 'ErrorCodeClass_uuid_111', - serviceHttpErrorCodes, - errors[1], - { - code: serviceHttpErrorCodes.Unknown, - error: errors[1] as any, - objectUUID: 'ErrorCodeClass_uuid_111' - }, - 0 - ], - [ - `error is instanceof Error`, - 'ErrorCodeClass_uuid_112', - serviceHttpErrorCodes, - errors[2], - { - code: serviceHttpErrorCodes.Unknown, - error: errors[2] as any, - objectUUID: 'ErrorCodeClass_uuid_112' - }, - 0 - ] - ]; + describe("should verify will return ErrorRecord for various cases when serviceHttpErrorRecords are provided and error is not HttpErrorResponse", () => { + // Given + const serviceHttpErrorCodes: Record = { + All: "ErrorCodeClass_SpiedPublicName_andMethod_", + ClientErrors: "ErrorCodeClass_SpiedPublicName_andMethod_4\\d\\d", + BadRequest: "ErrorCodeClass_SpiedPublicName_andMethod_400", + Unauthorized: "ErrorCodeClass_SpiedPublicName_andMethod_401", + Forbidden: "ErrorCodeClass_SpiedPublicName_andMethod_403", + NotFound: "ErrorCodeClass_SpiedPublicName_andMethod_404", + MethodNotAllowed: "ErrorCodeClass_SpiedPublicName_andMethod_405", + Conflict: "ErrorCodeClass_SpiedPublicName_andMethod_409", + UnprocessableEntity: "ErrorCodeClass_SpiedPublicName_andMethod_422", + ServerErrors: "ErrorCodeClass_SpiedPublicName_andMethod_5\\d\\d", + InternalServerError: "ErrorCodeClass_SpiedPublicName_andMethod_500", + ServiceUnavailable: "ErrorCodeClass_SpiedPublicName_andMethod_503", + Unknown: "ErrorCodeClass_SpiedPublicName_andMethod_unknown", + }; + const errors: unknown[] = [ + new Error(`Random Error 1`), + null, + new Error(`Random Error 3`), + ]; + const params: Array< + [ + string, + string, + Record, + unknown, + ErrorRecord, + number, + ] + > = [ + [ + `error is instanceof Error`, + null, + serviceHttpErrorCodes, + errors[0], + { + code: serviceHttpErrorCodes.Unknown, + error: errors[0] as any, + objectUUID: "ErrorCodeClass_uuid", + }, + 1, + ], + [ + `error is null`, + "ErrorCodeClass_uuid_111", + serviceHttpErrorCodes, + errors[1], + { + code: serviceHttpErrorCodes.Unknown, + error: errors[1] as any, + objectUUID: "ErrorCodeClass_uuid_111", + }, + 0, + ], + [ + `error is instanceof Error`, + "ErrorCodeClass_uuid_112", + serviceHttpErrorCodes, + errors[2], + { + code: serviceHttpErrorCodes.Unknown, + error: errors[2] as any, + objectUUID: "ErrorCodeClass_uuid_112", + }, + 0, + ], + ]; - for (const [_description, _objectUUID, _serviceHttpErrorCodes, _error, _assertion, _numberOfInvoke] of params) { - it(`${_description}`, () => { - // Given - const spyGenerateObjectUUID = spyOn(CollectionsUtil, 'generateObjectUUID').and.returnValue('ErrorCodeClass_uuid'); + for (const [ + _description, + _objectUUID, + _serviceHttpErrorCodes, + _error, + _assertion, + _numberOfInvoke, + ] of params) { + it(`${_description}`, () => { + // Given + const spyGenerateObjectUUID = spyOn( + CollectionsUtil, + "generateObjectUUID", + ).and.returnValue("ErrorCodeClass_uuid"); - // When - const _errorRecord = processServiceRequestError(_objectUUID, _serviceHttpErrorCodes, _error); + // When + const _errorRecord = processServiceRequestError( + _objectUUID, + _serviceHttpErrorCodes, + _error, + ); - // Then - expect(_errorRecord).toEqual(_assertion); - expect(spyGenerateObjectUUID).toHaveBeenCalledTimes(_numberOfInvoke); - if (_numberOfInvoke > 0) { - expect(spyGenerateObjectUUID).toHaveBeenCalledWith('UnknownClassName'); - } - }); + // Then + expect(_errorRecord).toEqual(_assertion); + expect(spyGenerateObjectUUID).toHaveBeenCalledTimes(_numberOfInvoke); + if (_numberOfInvoke > 0) { + expect(spyGenerateObjectUUID).toHaveBeenCalledWith( + "UnknownClassName", + ); } - }); + }); + } + }); }); -describe('getApiFormattedErrorMessage', () => { - describe('parameterized_test', () => { - const errors: HttpErrorResponse[] = [ - new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: `Something bad happened and it's string` - }), - new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: { - what: `text what`, - why: `text why`, - consequences: `text consequences`, - countermeasures: `text countermeasures` - } - }), - new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: null - }), - new HttpErrorResponse({ - status: HttpStatusCode.BadGateway, - error: undefined - }), - new SyntaxError('Unsupported Action') as HttpErrorResponse - ]; - const params: Array<[string, Error, ApiErrorMessage]> = [ - [ - 'error is HttpErrorResponse and nested error is string', - errors[0], - { what: `${errors[0].error}`, why: `${errors[0].message}` } - ], - [ - 'error is HttpErrorResponse and nested error is formatted ApiErrorMessage', - errors[1], - { - what: `${errors[1].error.what}`, - why: `${errors[1].error.why}`, - consequences: `${errors[1].error.consequences}`, - countermeasures: `${errors[1].error.countermeasures}` - } - ], - [ - 'error is HttpErrorResponse and nested error is null', - errors[2], - { - what: 'Please contact Superollider and report the issue', - why: 'Internal Server Error' - } - ], - [ - 'error is HttpErrorResponse and nested error is undefined', - errors[3], - { - what: 'Please contact Superollider and report the issue', - why: 'Unknown Error' - } - ], - [ - 'error is not HttpErrorResponse', - errors[4], - { - what: 'Please contact Superollider and report the issue', - why: 'Unknown Error' - } - ] - ]; +describe("getApiFormattedErrorMessage", () => { + describe("parameterized_test", () => { + const errors: HttpErrorResponse[] = [ + new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: `Something bad happened and it's string`, + }), + new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: { + what: `text what`, + why: `text why`, + consequences: `text consequences`, + countermeasures: `text countermeasures`, + }, + }), + new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: null, + }), + new HttpErrorResponse({ + status: HttpStatusCode.BadGateway, + error: undefined, + }), + new SyntaxError("Unsupported Action") as HttpErrorResponse, + ]; + const params: Array<[string, Error, ApiErrorMessage]> = [ + [ + "error is HttpErrorResponse and nested error is string", + errors[0], + { what: `${errors[0].error}`, why: `${errors[0].message}` }, + ], + [ + "error is HttpErrorResponse and nested error is formatted ApiErrorMessage", + errors[1], + { + what: `${errors[1].error.what}`, + why: `${errors[1].error.why}`, + consequences: `${errors[1].error.consequences}`, + countermeasures: `${errors[1].error.countermeasures}`, + }, + ], + [ + "error is HttpErrorResponse and nested error is null", + errors[2], + { + what: "Please contact Superollider and report the issue", + why: "Internal Server Error", + }, + ], + [ + "error is HttpErrorResponse and nested error is undefined", + errors[3], + { + what: "Please contact Superollider and report the issue", + why: "Unknown Error", + }, + ], + [ + "error is not HttpErrorResponse", + errors[4], + { + what: "Please contact Superollider and report the issue", + why: "Unknown Error", + }, + ], + ]; - for (const [description, error, assertion] of params) { - it(`should verify will return ApiErrorMessage when ${description}`, () => { - // When - const apiMessage = getApiFormattedErrorMessage(error); + for (const [description, error, assertion] of params) { + it(`should verify will return ApiErrorMessage when ${description}`, () => { + // When + const apiMessage = getApiFormattedErrorMessage(error); - // Then - expect(apiMessage).toEqual(assertion); - }); - } - }); + // Then + expect(apiMessage).toEqual(assertion); + }); + } + }); }); -describe('getHumanReadableStatusText', () => { - describe('parameterized_test', () => { - const params: Array<[HttpStatusCode, string]> = [ - [HttpStatusCode.BadRequest, 'Invalid param'], - [HttpStatusCode.Unauthorized, 'Unauthorized'], - [HttpStatusCode.Forbidden, 'Forbidden'], - [HttpStatusCode.NotFound, 'Not Found'], - [HttpStatusCode.MethodNotAllowed, 'Not Allowed'], - [HttpStatusCode.Conflict, 'Conflict'], - [HttpStatusCode.UnprocessableEntity, 'Invalid operation'], - [HttpStatusCode.InternalServerError, 'Internal Server Error'], - [HttpStatusCode.ServiceUnavailable, 'Service Unavailable'], - [HttpStatusCode.BadGateway, 'Unknown Error'] - ]; +describe("getHumanReadableStatusText", () => { + describe("parameterized_test", () => { + const params: Array<[HttpStatusCode, string]> = [ + [HttpStatusCode.BadRequest, "Invalid param"], + [HttpStatusCode.Unauthorized, "Unauthorized"], + [HttpStatusCode.Forbidden, "Forbidden"], + [HttpStatusCode.NotFound, "Not Found"], + [HttpStatusCode.MethodNotAllowed, "Not Allowed"], + [HttpStatusCode.Conflict, "Conflict"], + [HttpStatusCode.UnprocessableEntity, "Invalid operation"], + [HttpStatusCode.InternalServerError, "Internal Server Error"], + [HttpStatusCode.ServiceUnavailable, "Service Unavailable"], + [HttpStatusCode.BadGateway, "Unknown Error"], + ]; - for (const [statusCode, assertion] of params) { - it(`should verify will return "${assertion}" for status code "${statusCode}"`, () => { - // When - const returnedText = getHumanReadableStatusText(statusCode); + for (const [statusCode, assertion] of params) { + it(`should verify will return "${assertion}" for status code "${statusCode}"`, () => { + // When + const returnedText = getHumanReadableStatusText(statusCode); - // Then - expect(returnedText).toEqual(assertion); - }); - } - }); + // Then + expect(returnedText).toEqual(assertion); + }); + } + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.ts index 286ad31805..85efa2e6f4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/error.utils.ts @@ -3,11 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; +import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { ApiErrorMessage, ErrorRecord, generateErrorCode, ServiceHttpErrorCodes } from '../../../common'; +import { + ApiErrorMessage, + ErrorRecord, + generateErrorCode, + ServiceHttpErrorCodes, +} from "../../../common"; /** * ** Process service HTTP request error and return ErrorRecord. @@ -17,126 +22,133 @@ import { ApiErrorMessage, ErrorRecord, generateErrorCode, ServiceHttpErrorCodes * @param {unknown} error - is actual error object reference */ export const processServiceRequestError = ( - objectUUID: string, - serviceHttpErrorCodes: Record, - error: unknown + objectUUID: string, + serviceHttpErrorCodes: Record, + error: unknown, ): ErrorRecord => { - const _objectUUID = CollectionsUtil.isDefined(objectUUID) ? objectUUID : CollectionsUtil.generateObjectUUID('UnknownClassName'); + const _objectUUID = CollectionsUtil.isDefined(objectUUID) + ? objectUUID + : CollectionsUtil.generateObjectUUID("UnknownClassName"); - if (!CollectionsUtil.isLiteralObject(serviceHttpErrorCodes)) { - return { - code: generateErrorCode('UnknownClassName', 'UnknownPublicName', 'UnknownMethodName', 'Generic'), - objectUUID: _objectUUID, - error: error instanceof Error ? error : null - }; - } - - if (error instanceof HttpErrorResponse) { - let code: string; + if (!CollectionsUtil.isLiteralObject(serviceHttpErrorCodes)) { + return { + code: generateErrorCode( + "UnknownClassName", + "UnknownPublicName", + "UnknownMethodName", + "Generic", + ), + objectUUID: _objectUUID, + error: error instanceof Error ? error : null, + }; + } - switch (error.status) { - case HttpStatusCode.BadRequest: - code = serviceHttpErrorCodes.BadRequest; - break; - case HttpStatusCode.Unauthorized: - code = serviceHttpErrorCodes.Unauthorized; - break; - case HttpStatusCode.Forbidden: - code = serviceHttpErrorCodes.Forbidden; - break; - case HttpStatusCode.NotFound: - code = serviceHttpErrorCodes.NotFound; - break; - case HttpStatusCode.MethodNotAllowed: - code = serviceHttpErrorCodes.MethodNotAllowed; - break; - case HttpStatusCode.Conflict: - code = serviceHttpErrorCodes.Conflict; - break; - case HttpStatusCode.UnprocessableEntity: - code = serviceHttpErrorCodes.UnprocessableEntity; - break; - case HttpStatusCode.InternalServerError: - code = serviceHttpErrorCodes.InternalServerError; - break; - case HttpStatusCode.ServiceUnavailable: - code = serviceHttpErrorCodes.ServiceUnavailable; - break; - default: - code = serviceHttpErrorCodes.Unknown; - } + if (error instanceof HttpErrorResponse) { + let code: string; - return { - code, - objectUUID: _objectUUID, - error, - httpStatusCode: error.status - }; + switch (error.status) { + case HttpStatusCode.BadRequest: + code = serviceHttpErrorCodes.BadRequest; + break; + case HttpStatusCode.Unauthorized: + code = serviceHttpErrorCodes.Unauthorized; + break; + case HttpStatusCode.Forbidden: + code = serviceHttpErrorCodes.Forbidden; + break; + case HttpStatusCode.NotFound: + code = serviceHttpErrorCodes.NotFound; + break; + case HttpStatusCode.MethodNotAllowed: + code = serviceHttpErrorCodes.MethodNotAllowed; + break; + case HttpStatusCode.Conflict: + code = serviceHttpErrorCodes.Conflict; + break; + case HttpStatusCode.UnprocessableEntity: + code = serviceHttpErrorCodes.UnprocessableEntity; + break; + case HttpStatusCode.InternalServerError: + code = serviceHttpErrorCodes.InternalServerError; + break; + case HttpStatusCode.ServiceUnavailable: + code = serviceHttpErrorCodes.ServiceUnavailable; + break; + default: + code = serviceHttpErrorCodes.Unknown; } return { - code: serviceHttpErrorCodes.Unknown, - objectUUID: _objectUUID, - error: error instanceof Error ? error : null + code, + objectUUID: _objectUUID, + error, + httpStatusCode: error.status, }; + } + + return { + code: serviceHttpErrorCodes.Unknown, + objectUUID: _objectUUID, + error: error instanceof Error ? error : null, + }; }; /** * ** Get API formatted error message from provided Error. */ export const getApiFormattedErrorMessage = (error: Error): ApiErrorMessage => { - let statusCode: number = null; - - if (error instanceof HttpErrorResponse) { - if (CollectionsUtil.isString(error.error)) { - return { - what: `${error.error}`, - why: `${error.message}` - }; - } + let statusCode: number = null; - if (CollectionsUtil.isLiteralObject(error.error)) { - return { - what: `${(error.error as ApiErrorMessage).what}`, - why: `${(error.error as ApiErrorMessage).why}`, - consequences: `${(error.error as ApiErrorMessage).consequences}`, - countermeasures: `${(error.error as ApiErrorMessage).countermeasures}` - }; - } + if (error instanceof HttpErrorResponse) { + if (CollectionsUtil.isString(error.error)) { + return { + what: `${error.error}`, + why: `${error.message}`, + }; + } - statusCode = error.status; + if (CollectionsUtil.isLiteralObject(error.error)) { + return { + what: `${(error.error as ApiErrorMessage).what}`, + why: `${(error.error as ApiErrorMessage).why}`, + consequences: `${(error.error as ApiErrorMessage).consequences}`, + countermeasures: `${(error.error as ApiErrorMessage).countermeasures}`, + }; } - return { - what: 'Please contact Superollider and report the issue', - why: getHumanReadableStatusText(statusCode) - }; + statusCode = error.status; + } + + return { + what: "Please contact Superollider and report the issue", + why: getHumanReadableStatusText(statusCode), + }; }; /** * ** Get Human readable text from HTTP error status code. */ export const getHumanReadableStatusText = (httpErrorStatus: number): string => { - switch (httpErrorStatus) { - case HttpStatusCode.BadRequest: - return 'Invalid param'; - case HttpStatusCode.Unauthorized: - return 'Unauthorized'; - case HttpStatusCode.Forbidden: - return 'Forbidden'; - case HttpStatusCode.NotFound: - return 'Not Found'; - case HttpStatusCode.MethodNotAllowed: - return 'Not Allowed'; - case HttpStatusCode.Conflict: - return 'Conflict'; - case HttpStatusCode.UnprocessableEntity: - return 'Invalid operation'; - case HttpStatusCode.InternalServerError: - return 'Internal Server Error'; - case HttpStatusCode.ServiceUnavailable: - return 'Service Unavailable'; - default: - return 'Unknown Error'; - } + switch (httpErrorStatus) { + case HttpStatusCode.BadRequest: + return "Invalid param"; + case HttpStatusCode.Unauthorized: + return "Unauthorized"; + case HttpStatusCode.Forbidden: + return "Forbidden"; + case HttpStatusCode.NotFound: + return "Not Found"; + case HttpStatusCode.MethodNotAllowed: + return "Not Allowed"; + case HttpStatusCode.Conflict: + return "Conflict"; + case HttpStatusCode.UnprocessableEntity: + return "Invalid operation"; + case HttpStatusCode.InternalServerError: + return "Internal Server Error"; + case HttpStatusCode.ServiceUnavailable: + return "Service Unavailable"; + default: + return "Unknown Error"; + } }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/index.ts index bb8278bb1e..b9a1cc8a50 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/error/utils/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './error.utils'; +export * from "./error.utils"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts index c82cfd3841..7f1dc51284 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component'; -export * from './error'; -export * from './navigation'; -export * from './ngrx'; -export * from './router'; -export * from './system-events'; -export * from './url-state-manager'; +export * from "./component"; +export * from "./error"; +export * from "./navigation"; +export * from "./ngrx"; +export * from "./router"; +export * from "./system-events"; +export * from "./url-state-manager"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts index de62709897..cda649c573 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './services'; +export * from "./services"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts index 0f016b3f17..3e8b1691b8 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { NavigationService } from './index'; +export { NavigationService } from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts index 2c92468b27..c8beba7ca1 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './navigation.service'; +export * from "./navigation.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts index ceb61dac86..271eec577a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts @@ -5,384 +5,463 @@ /* eslint-disable @typescript-eslint/dot-notation */ -import { NavigationExtras, Router } from '@angular/router'; -import { TestBed } from '@angular/core/testing'; +import { NavigationExtras, Router } from "@angular/router"; +import { TestBed } from "@angular/core/testing"; -import { of } from 'rxjs'; +import { of } from "rxjs"; -import { TaurusRouteData } from '../../../common'; +import { TaurusRouteData } from "../../../common"; -import { RouterService, RouteSegments, RouteState } from '../../router'; +import { RouterService, RouteSegments, RouteState } from "../../router"; -import { NavigationService } from './navigation.service'; +import { NavigationService } from "./navigation.service"; -describe('NavigationService', () => { - let routerStub: jasmine.SpyObj; - let routerServiceStub: jasmine.SpyObj; +describe("NavigationService", () => { + let routerStub: jasmine.SpyObj; + let routerServiceStub: jasmine.SpyObj; - let service: NavigationService; + let service: NavigationService; - beforeEach(() => { - routerStub = jasmine.createSpyObj('router', ['navigate']); - routerServiceStub = jasmine.createSpyObj('routerService', ['getState', 'initialize']); + beforeEach(() => { + routerStub = jasmine.createSpyObj("router", ["navigate"]); + routerServiceStub = jasmine.createSpyObj("routerService", [ + "getState", + "initialize", + ]); - TestBed.configureTestingModule({ - providers: [ - { provide: Router, useValue: routerStub }, - { provide: RouterService, useValue: routerServiceStub }, - NavigationService - ] - }); + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: routerStub }, + { provide: RouterService, useValue: routerServiceStub }, + NavigationService, + ], + }); - service = TestBed.inject(NavigationService); + service = TestBed.inject(NavigationService); - // @ts-ignore - service['router'] = routerStub; - // @ts-ignore - service['routerService'] = routerServiceStub; + // @ts-ignore + service["router"] = routerStub; + // @ts-ignore + service["routerService"] = routerServiceStub; + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + }); + + describe("Methods::()", () => { + describe("|initialize|", () => { + it("should verify method exist and is callable", () => { + // Then + expect(service.initialize).toBeDefined(); + expect(() => service.initialize()).not.toThrow(); + expect(routerServiceStub.initialize).toHaveBeenCalled(); + }); }); - it('should verify instance is created', () => { + describe("|navigate|", () => { + let url: string; + let urlChunks: string[]; + let extras: NavigationExtras; + + beforeEach(() => { + url = "/explore/data-jobs"; + urlChunks = ["explore", "data-jobs"]; + extras = { + queryParams: { + search: "vdk", + }, + replaceUrl: true, + queryParamsHandling: "merge", + }; + // @ts-ignore + service["router"] = routerStub; + }); + + it("should verify will call Router.navigate() from Angular", () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.navigate(url); + + // Then + expect(routerStub.navigate).toHaveBeenCalledWith([ + "/explore", + "data-jobs", + ]); + }); + + it("should verify will return Promise", () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + const returnedValue = service.navigate(url); + + // Then + expect(returnedValue).toBeInstanceOf(Promise); + }); + + it("should verify will return Promise.resolve(false) on Nil value for url", () => { + // When + const returnedValue = service.navigate(null); + + // Then + returnedValue.then((v) => expect(v).toBeFalse()); + expect(routerStub.navigate).not.toHaveBeenCalled(); + }); + + it("should verify will handle both url string and url Array", () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.navigate(url); + service.navigate(urlChunks); + // Then - expect(service).toBeDefined(); + expect(routerStub.navigate).toHaveBeenCalledTimes(2); + expect(routerStub.navigate.calls.argsFor(0)).toEqual([ + ["/explore", "data-jobs"], + ]); + expect(routerStub.navigate.calls.argsFor(1)).toEqual([ + ["/explore", "data-jobs"], + ]); + }); + + it("should verify will handle extras parameters", () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.navigate(url, extras); + + // Then + expect(routerStub.navigate).toHaveBeenCalledWith( + ["/explore", "data-jobs"], + extras, + ); + }); }); - describe('Methods::()', () => { - describe('|initialize|', () => { - it('should verify method exist and is callable', () => { - // Then - expect(service.initialize).toBeDefined(); - expect(() => service.initialize()).not.toThrow(); - expect(routerServiceStub.initialize).toHaveBeenCalled(); - }); - }); + describe("|_navigationSystemEventHandler_|", () => { + it("should verify will skip navigation if payload is Nil", () => { + // Given + const payload: { url: string | string[]; extras?: NavigationExtras } = + null; + const navigateSpy = spyOn(service, "navigate").and.callFake(() => + Promise.resolve(false), + ); - describe('|navigate|', () => { - let url: string; - let urlChunks: string[]; - let extras: NavigationExtras; - - beforeEach(() => { - url = '/explore/data-jobs'; - urlChunks = ['explore', 'data-jobs']; - extras = { - queryParams: { - search: 'vdk' - }, - replaceUrl: true, - queryParamsHandling: 'merge' - }; - // @ts-ignore - service['router'] = routerStub; - }); - - it('should verify will call Router.navigate() from Angular', () => { - // Given - routerStub.navigate.and.returnValue(Promise.resolve(true)); - - // When - service.navigate(url); - - // Then - expect(routerStub.navigate).toHaveBeenCalledWith(['/explore', 'data-jobs']); - }); - - it('should verify will return Promise', () => { - // Given - routerStub.navigate.and.returnValue(Promise.resolve(true)); - - // When - const returnedValue = service.navigate(url); - - // Then - expect(returnedValue).toBeInstanceOf(Promise); - }); - - it('should verify will return Promise.resolve(false) on Nil value for url', () => { - // When - const returnedValue = service.navigate(null); - - // Then - returnedValue.then((v) => expect(v).toBeFalse()); - expect(routerStub.navigate).not.toHaveBeenCalled(); - }); - - it('should verify will handle both url string and url Array', () => { - // Given - routerStub.navigate.and.returnValue(Promise.resolve(true)); - - // When - service.navigate(url); - service.navigate(urlChunks); - - // Then - expect(routerStub.navigate).toHaveBeenCalledTimes(2); - expect(routerStub.navigate.calls.argsFor(0)).toEqual([['/explore', 'data-jobs']]); - expect(routerStub.navigate.calls.argsFor(1)).toEqual([['/explore', 'data-jobs']]); - }); - - it('should verify will handle extras parameters', () => { - // Given - routerStub.navigate.and.returnValue(Promise.resolve(true)); - - // When - service.navigate(url, extras); - - // Then - expect(routerStub.navigate).toHaveBeenCalledWith(['/explore', 'data-jobs'], extras); - }); - }); + // When + const returnedValue = service._navigationSystemEventHandler_(payload); + + // Then + returnedValue.then((v) => expect(v).toBeFalse()); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + + it("should verify will execute navigation when payload is provided", () => { + // Given + const payload = { + url: "/explore/data-jobs", + extras: { queryParams: { search: "vdk" } }, + }; + spyOn(service, "navigate").and.callFake(() => Promise.resolve(true)); + + // When + const returnedValue = service._navigationSystemEventHandler_(payload); - describe('|_navigationSystemEventHandler_|', () => { - it('should verify will skip navigation if payload is Nil', () => { - // Given - const payload: { url: string | string[]; extras?: NavigationExtras } = null; - const navigateSpy = spyOn(service, 'navigate').and.callFake(() => Promise.resolve(false)); + // Then + returnedValue.then((v) => expect(v).toBeTrue()); + expect(service.navigate).toHaveBeenCalledWith( + payload.url, + payload.extras, + ); + }); + }); - // When - const returnedValue = service._navigationSystemEventHandler_(payload); + describe("|navigateBack|", () => { + it("should verify will invoke correct methods when NavigationAction provided", (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + "delivery", + { + navigateBack: { + path: "$.parent", + queryParamsHandling: "preserve", + queryParams: { randomParam: "abc" }, + }, + } as TaurusRouteData, + {}, + { search: "team-test" }, + RouteSegments.of( + "entity/25", + { + navigateTo: { + path: "/domain/context/entity/{0}/update/information", + replacers: [{ searchValue: "{0}", replaceValue: "$.entity" }], + queryParams: { showWizard: true }, + }, + } as TaurusRouteData, + { entityId: 25 }, + { search: "team-test" }, + RouteSegments.of( + "domain/context", + {}, + {}, + { search: "team-test" }, + null, + "domain/context", + ), + "entity/:entityId", + ), + "delivery", + ), + "domain/context/entity/25/delivery", + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, "navigate").and.returnValue( + Promise.resolve(true), + ); + + // When + const promise = service.navigateBack(); - // Then - returnedValue.then((v) => expect(v).toBeFalse()); - expect(navigateSpy).not.toHaveBeenCalled(); - }); + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + "/domain/context/entity/25", + { + queryParamsHandling: "preserve", + }, + ); + + done(); + }); + }); + + it("should verify will invoke correct methods when no NavigationAction provided", (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + "delivery", + {}, + {}, + { search: "team-test" }, + RouteSegments.of( + "entity/25", + {}, + { entityId: 25 }, + { search: "team-test" }, + RouteSegments.of( + "domain/context", + {}, + {}, + { search: "team-test" }, + null, + "domain/context", + ), + "entity/:entityId", + ), + "delivery", + ), + "domain/context/entity/25/delivery", + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, "navigate").and.returnValue( + Promise.resolve(true), + ); + + // When + const promise = service.navigateBack(); - it('should verify will execute navigation when payload is provided', () => { - // Given - const payload = { url: '/explore/data-jobs', extras: { queryParams: { search: 'vdk' } } }; - spyOn(service, 'navigate').and.callFake(() => Promise.resolve(true)); + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + "/domain/context/entity/25", + { + queryParamsHandling: "merge", + }, + ); + + done(); + }); + }); + }); - // When - const returnedValue = service._navigationSystemEventHandler_(payload); + describe("|navigateTo|", () => { + it("should verify will invoke correct methods preserve routerState QueryParams", (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + "delivery", + { + navigateBack: { + path: "$.parent", + queryParamsHandling: "preserve", + queryParams: { randomParam: "abc" }, + }, + } as TaurusRouteData, + {}, + { search: "team-test" }, + RouteSegments.of( + "entity/25", + { + navigateTo: { + path: "/domain/context/entity/{0}/update/information", + replacers: [{ searchValue: "{0}", replaceValue: "$.entity" }], + queryParamsHandling: "preserve", + queryParams: { showWizard: true }, + }, + } as TaurusRouteData, + { entityId: 25 }, + { search: "team-test" }, + RouteSegments.of( + "domain/context", + {}, + {}, + { search: "team-test" }, + null, + "domain/context", + ), + "entity/:entityId", + ), + "delivery", + ), + "domain/context/entity/25/delivery", + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, "navigate").and.returnValue( + Promise.resolve(true), + ); + + // When + const promise = service.navigateTo({ "$.entity": "101" }); - // Then - returnedValue.then((v) => expect(v).toBeTrue()); - expect(service.navigate).toHaveBeenCalledWith(payload.url, payload.extras); - }); + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + "/domain/context/entity/101/update/information", + { + queryParamsHandling: "preserve", + }, + ); + + done(); }); + }); + + it("should verify will invoke correct methods merge QueryParams, provided and from routeState", (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + "delivery", + { + navigateBack: { + path: "$.parent", + queryParamsHandling: "preserve", + queryParams: { randomParam: "abc" }, + }, + } as TaurusRouteData, + {}, + { search: "team-test" }, + RouteSegments.of( + "entity/25", + { + navigateTo: { + path: "/domain/context/entity/{0}/update/information", + replacers: [{ searchValue: "{0}", replaceValue: "$.entity" }], + queryParams: { showWizard: true }, + queryParamsHandling: "merge", + }, + } as TaurusRouteData, + { entityId: 25 }, + { search: "team-test" }, + RouteSegments.of( + "domain/context", + {}, + {}, + { search: "team-test" }, + null, + "domain/context", + ), + "entity/:entityId", + ), + "delivery", + ), + "domain/context/entity/25/delivery", + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, "navigate").and.returnValue( + Promise.resolve(true), + ); + + // When + const promise = service.navigateTo({ "$.entity": "101" }); - describe('|navigateBack|', () => { - it('should verify will invoke correct methods when NavigationAction provided', (done) => { - // Given - const routeState = RouteState.of( - RouteSegments.of( - 'delivery', - { - navigateBack: { - path: '$.parent', - queryParamsHandling: 'preserve', - queryParams: { randomParam: 'abc' } - } - } as TaurusRouteData, - {}, - { search: 'team-test' }, - RouteSegments.of( - 'entity/25', - { - navigateTo: { - path: '/domain/context/entity/{0}/update/information', - replacers: [{ searchValue: '{0}', replaceValue: '$.entity' }], - queryParams: { showWizard: true } - } - } as TaurusRouteData, - { entityId: 25 }, - { search: 'team-test' }, - RouteSegments.of('domain/context', {}, {}, { search: 'team-test' }, null, 'domain/context'), - 'entity/:entityId' - ), - 'delivery' - ), - 'domain/context/entity/25/delivery' - ); - routerServiceStub.getState.and.returnValue(of(routeState)); - const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); - - // When - const promise = service.navigateBack(); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(routerServiceStub.getState).toHaveBeenCalled(); - promise.then((value) => { - expect(value).toBeTrue(); - expect(navigateSpy).toHaveBeenCalledWith('/domain/context/entity/25', { - queryParamsHandling: 'preserve' - }); - - done(); - }); - }); - - it('should verify will invoke correct methods when no NavigationAction provided', (done) => { - // Given - const routeState = RouteState.of( - RouteSegments.of( - 'delivery', - {}, - {}, - { search: 'team-test' }, - RouteSegments.of( - 'entity/25', - {}, - { entityId: 25 }, - { search: 'team-test' }, - RouteSegments.of('domain/context', {}, {}, { search: 'team-test' }, null, 'domain/context'), - 'entity/:entityId' - ), - 'delivery' - ), - 'domain/context/entity/25/delivery' - ); - routerServiceStub.getState.and.returnValue(of(routeState)); - const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); - - // When - const promise = service.navigateBack(); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(routerServiceStub.getState).toHaveBeenCalled(); - promise.then((value) => { - expect(value).toBeTrue(); - expect(navigateSpy).toHaveBeenCalledWith('/domain/context/entity/25', { - queryParamsHandling: 'merge' - }); - - done(); - }); - }); + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + "/domain/context/entity/101/update/information", + { + queryParamsHandling: "merge", + queryParams: { search: "team-test", showWizard: true }, + }, + ); + + done(); }); + }); + + it("should verify will reject Promise when no NavigationAction is provided", (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + "delivery", + {}, + {}, + { search: "team-test" }, + null, + "delivery", + ), + "domain/context/entity/25/delivery", + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, "navigate").and.returnValue( + Promise.resolve(true), + ); + const consoleErrorSpy = spyOn(console, "error").and.callThrough(); + + // When + const promise = service.navigateTo(); - describe('|navigateTo|', () => { - it('should verify will invoke correct methods preserve routerState QueryParams', (done) => { - // Given - const routeState = RouteState.of( - RouteSegments.of( - 'delivery', - { - navigateBack: { - path: '$.parent', - queryParamsHandling: 'preserve', - queryParams: { randomParam: 'abc' } - } - } as TaurusRouteData, - {}, - { search: 'team-test' }, - RouteSegments.of( - 'entity/25', - { - navigateTo: { - path: '/domain/context/entity/{0}/update/information', - replacers: [{ searchValue: '{0}', replaceValue: '$.entity' }], - queryParamsHandling: 'preserve', - queryParams: { showWizard: true } - } - } as TaurusRouteData, - { entityId: 25 }, - { search: 'team-test' }, - RouteSegments.of('domain/context', {}, {}, { search: 'team-test' }, null, 'domain/context'), - 'entity/:entityId' - ), - 'delivery' - ), - 'domain/context/entity/25/delivery' - ); - routerServiceStub.getState.and.returnValue(of(routeState)); - const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); - - // When - const promise = service.navigateTo({ '$.entity': '101' }); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(routerServiceStub.getState).toHaveBeenCalled(); - promise.then((value) => { - expect(value).toBeTrue(); - expect(navigateSpy).toHaveBeenCalledWith('/domain/context/entity/101/update/information', { - queryParamsHandling: 'preserve' - }); - - done(); - }); - }); - - it('should verify will invoke correct methods merge QueryParams, provided and from routeState', (done) => { - // Given - const routeState = RouteState.of( - RouteSegments.of( - 'delivery', - { - navigateBack: { - path: '$.parent', - queryParamsHandling: 'preserve', - queryParams: { randomParam: 'abc' } - } - } as TaurusRouteData, - {}, - { search: 'team-test' }, - RouteSegments.of( - 'entity/25', - { - navigateTo: { - path: '/domain/context/entity/{0}/update/information', - replacers: [{ searchValue: '{0}', replaceValue: '$.entity' }], - queryParams: { showWizard: true }, - queryParamsHandling: 'merge' - } - } as TaurusRouteData, - { entityId: 25 }, - { search: 'team-test' }, - RouteSegments.of('domain/context', {}, {}, { search: 'team-test' }, null, 'domain/context'), - 'entity/:entityId' - ), - 'delivery' - ), - 'domain/context/entity/25/delivery' - ); - routerServiceStub.getState.and.returnValue(of(routeState)); - const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); - - // When - const promise = service.navigateTo({ '$.entity': '101' }); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(routerServiceStub.getState).toHaveBeenCalled(); - promise.then((value) => { - expect(value).toBeTrue(); - expect(navigateSpy).toHaveBeenCalledWith('/domain/context/entity/101/update/information', { - queryParamsHandling: 'merge', - queryParams: { search: 'team-test', showWizard: true } - }); - - done(); - }); - }); - - it('should verify will reject Promise when no NavigationAction is provided', (done) => { - // Given - const routeState = RouteState.of( - RouteSegments.of('delivery', {}, {}, { search: 'team-test' }, null, 'delivery'), - 'domain/context/entity/25/delivery' - ); - routerServiceStub.getState.and.returnValue(of(routeState)); - const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); - const consoleErrorSpy = spyOn(console, 'error').and.callThrough(); - - // When - const promise = service.navigateTo(); - - // Then - expect(promise).toBeInstanceOf(Promise); - expect(routerServiceStub.getState).toHaveBeenCalled(); - promise.then((value) => { - expect(value).toBeFalse(); - expect(navigateSpy).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalled(); - - done(); - }); - }); + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeFalse(); + expect(navigateSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalled(); + + done(); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts index 83e312d530..a970688a17 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts @@ -3,19 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; -import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router'; +import { Injectable } from "@angular/core"; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + NavigationExtras, + Router, +} from "@angular/router"; -import { take } from 'rxjs/operators'; +import { take } from "rxjs/operators"; -import { ArrayElement, CollectionsUtil } from '../../../utils'; +import { ArrayElement, CollectionsUtil } from "../../../utils"; -import { Replacer, TaurusNavigateAction } from '../../../common'; +import { Replacer, TaurusNavigateAction } from "../../../common"; -import { SE_NAVIGATE, SystemEventHandler, SystemEventHandlerClass, SystemEventNavigatePayload } from '../../system-events'; +import { + SE_NAVIGATE, + SystemEventHandler, + SystemEventHandlerClass, + SystemEventNavigatePayload, +} from "../../system-events"; -import { RouterService, RouteState } from '../../router'; -import { RouteStateFactory } from '../../router/factory'; +import { RouterService, RouteState } from "../../router"; +import { RouteStateFactory } from "../../router/factory"; /** * ** Service should be provided from the Root injector in Application. @@ -23,301 +33,374 @@ import { RouteStateFactory } from '../../router/factory'; @Injectable() @SystemEventHandlerClass() export class NavigationService { - private readonly _routeStateFactory: RouteStateFactory; - - /** - * ** Constructor. - */ - constructor( - private readonly router: Router, - private readonly routerService: RouterService - ) { - this._routeStateFactory = new RouteStateFactory(); + private readonly _routeStateFactory: RouteStateFactory; + + /** + * ** Constructor. + */ + constructor( + private readonly router: Router, + private readonly routerService: RouterService, + ) { + this._routeStateFactory = new RouteStateFactory(); + } + + /** + * ** Intercept SE_NAVIGATE Event and handle (react) on it. + */ + @SystemEventHandler(SE_NAVIGATE) + _navigationSystemEventHandler_( + payload: SystemEventNavigatePayload, + ): Promise { + if (CollectionsUtil.isNil(payload)) { + return Promise.resolve(false); } - /** - * ** Intercept SE_NAVIGATE Event and handle (react) on it. - */ - @SystemEventHandler(SE_NAVIGATE) - _navigationSystemEventHandler_(payload: SystemEventNavigatePayload): Promise { - if (CollectionsUtil.isNil(payload)) { - return Promise.resolve(false); - } - - return this.navigate(payload.url, payload.extras); + return this.navigate(payload.url, payload.extras); + } + + /** + * ** Navigate to url with provided extras. + */ + navigate( + url: string | string[], + extras?: NavigationExtras, + ): Promise { + let urlChunks: string[]; + + if (CollectionsUtil.isArray(url)) { + urlChunks = url.map((v, i) => (i === 0 ? `/${v.replace(/^\//, "")}` : v)); + } else if (CollectionsUtil.isString(url)) { + if (url.trim() === "/") { + urlChunks = ["/"]; + } else { + urlChunks = url + .split("/") + .filter((v) => !!v) + .map((v, i) => (i === 0 ? `/${v}` : v)); + } + } else { + return Promise.resolve(false); } - /** - * ** Navigate to url with provided extras. - */ - navigate(url: string | string[], extras?: NavigationExtras): Promise { - let urlChunks: string[]; - - if (CollectionsUtil.isArray(url)) { - urlChunks = url.map((v, i) => (i === 0 ? `/${v.replace(/^\//, '')}` : v)); - } else if (CollectionsUtil.isString(url)) { - if (url.trim() === '/') { - urlChunks = ['/']; - } else { - urlChunks = url - .split('/') - .filter((v) => !!v) - .map((v, i) => (i === 0 ? `/${v}` : v)); - } - } else { - return Promise.resolve(false); - } - - return CollectionsUtil.isLiteralObject(extras) ? this.router.navigate(urlChunks, extras) : this.router.navigate(urlChunks); + return CollectionsUtil.isLiteralObject(extras) + ? this.router.navigate(urlChunks, extras) + : this.router.navigate(urlChunks); + } + + /** + * ** Navigate to using Route config, RouteState and leveraging provided optional replaceValueResolver. + * + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

    + * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

    + *

    + * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + navigateTo( + replaceValues?: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot, + ): Promise { + return this._navigateWithAction( + replaceValues, + activatedRoute, + "navigateTo", + ); + } + + /** + * ** Navigate back using Route config, RouteState and leveraging provided optional replaceValueResolver. + * + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

    + * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

    + *

    + * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + navigateBack( + replaceValues?: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot, + ): Promise { + return this._navigateWithAction( + replaceValues, + activatedRoute, + "navigateBack", + ); + } + + /** + * ** Redirect using Route config, RouteState and leveraging provided optional replaceValueResolver. + * + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

    + * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

    + *

    + * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + redirect( + replaceValues?: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot, + ): Promise { + return this._navigateWithAction(replaceValues, activatedRoute, "redirect"); + } + + /** + * ** Resolve NavigationAction using Route config, RouteState and leveraging provided optional replaceValueResolver. + * @param navigateActionType is the type of the NavigationAction we want to resolve from Route config. + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

    + * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

    + *

    + * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + resolveNavigateActionUrl( + navigateActionType: "navigateTo" | "navigateBack" | "redirect", + replaceValues?: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot | RouteState, + ): Promise<[string, NavigationExtras]> { + const _replaceValues = CollectionsUtil.isLiteralObject(replaceValues) + ? replaceValues + : {}; + + return this._extractRouteState(activatedRoute) + .then((state) => + this._constructNavigateParameters(state, navigateActionType), + ) + .then((params) => + this._resolveNavigateValues( + params.url, + params.replacers, + _replaceValues, + params.navigationExtras, + ), + ); + } + + /** + * ** Initialize the Service without any operation, just to create singleton Service instance. + * + * - It should be done in the root of the project only once. + * - Possible place is AppComponent or some root initializer guard. + */ + initialize(): void { + this.routerService.initialize(); + } + + private _navigateWithAction( + replaceValues: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }, + activatedRoute: ActivatedRoute | ActivatedRouteSnapshot, + dataKey: "navigateTo" | "navigateBack" | "redirect", + ): Promise { + return this.resolveNavigateActionUrl(dataKey, replaceValues, activatedRoute) + .then() + .then(([url, navigationExtras]) => this.navigate(url, navigationExtras)) + .catch((error) => { + console.error(error); + + return false; + }); + } + + private _extractRouteState( + activatedRoute: ActivatedRoute | ActivatedRouteSnapshot | RouteState, + ): Promise { + // NOTICE + // this piece of code is needed for Guards cases usage. + // Use params and data from Guard RouteState instead from RouterService because + // RouterService state is populated after all guards are resolved + if (CollectionsUtil.isDefined(activatedRoute)) { + if (activatedRoute instanceof RouteState) { + return Promise.resolve(activatedRoute); + } + + return Promise.resolve( + this._routeStateFactory.create( + activatedRoute instanceof ActivatedRoute + ? activatedRoute.snapshot + : activatedRoute, + null, + ), + ); } - /** - * ** Navigate to using Route config, RouteState and leveraging provided optional replaceValueResolver. - * - * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. - * - What does it mean? - * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but - * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params - * could be key in this Map to construct correct url depending of the logic in RouteConfig. - *

    - * Important!!! - * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. - *

    - *

    - * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. - */ - navigateTo( - replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, - activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot - ): Promise { - return this._navigateWithAction(replaceValues, activatedRoute, 'navigateTo'); + return this.routerService + .getState() + .pipe(take(1)) + .toPromise() + .then((state) => state); + } + + // eslint-disable-next-line max-len + private _constructNavigateParameters( + state: RouteState, + dataKey: "navigateTo" | "navigateBack" | "redirect", + ): Promise { + const navigateAction = state.getData(dataKey); + const navigationExtras: NavigationExtras = { queryParamsHandling: "merge" }; + + let url: string; + let replacers: Array> = []; + + if (CollectionsUtil.isDefined(navigateAction)) { + url = this._resolveNavigateUrl(state, dataKey, navigateAction); + + if (CollectionsUtil.isArray(navigateAction.replacers)) { + replacers = navigateAction.replacers; + } + + this._appendNavigateExtras(state, navigationExtras, navigateAction); + } else if (dataKey === "navigateBack") { + url = state.getParentAbsoluteRoutePath(); + } else { + return Promise.reject("Cannot navigate without NavigationAction"); } - /** - * ** Navigate back using Route config, RouteState and leveraging provided optional replaceValueResolver. - * - * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. - * - What does it mean? - * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but - * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params - * could be key in this Map to construct correct url depending of the logic in RouteConfig. - *

    - * Important!!! - * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. - *

    - *

    - * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. - */ - navigateBack( - replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, - activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot - ): Promise { - return this._navigateWithAction(replaceValues, activatedRoute, 'navigateBack'); + return Promise.resolve({ + url, + replacers, + navigationExtras, + }); + } + + private _resolveNavigateUrl( + state: RouteState, + dataKey: "navigateTo" | "navigateBack" | "redirect", + navigateAction: TaurusNavigateAction, + ): string { + if (dataKey === "redirect" || dataKey === "navigateBack") { + if ( + navigateAction.path === "$.parent" || + navigateAction.path === "$.requested" + ) { + return navigateAction.useConfigPath + ? state.getParentAbsoluteConfigPath() + : state.getParentAbsoluteRoutePath(); + } + + if (navigateAction.path === "$.current") { + return navigateAction.useConfigPath + ? state.getAbsoluteConfigPath() + : state.getAbsoluteRoutePath(); + } + + return navigateAction.path; } - /** - * ** Redirect using Route config, RouteState and leveraging provided optional replaceValueResolver. - * - * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. - * - What does it mean? - * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but - * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params - * could be key in this Map to construct correct url depending of the logic in RouteConfig. - *

    - * Important!!! - * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. - *

    - *

    - * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. - */ - redirect( - replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, - activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot - ): Promise { - return this._navigateWithAction(replaceValues, activatedRoute, 'redirect'); + if (navigateAction.path === "$.current") { + return navigateAction.useConfigPath + ? state.getAbsoluteConfigPath() + : state.getAbsoluteRoutePath(); } - /** - * ** Resolve NavigationAction using Route config, RouteState and leveraging provided optional replaceValueResolver. - * @param navigateActionType is the type of the NavigationAction we want to resolve from Route config. - * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. - * - What does it mean? - * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but - * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params - * could be key in this Map to construct correct url depending of the logic in RouteConfig. - *

    - * Important!!! - * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. - *

    - *

    - * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. - */ - resolveNavigateActionUrl( - navigateActionType: 'navigateTo' | 'navigateBack' | 'redirect', - replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, - activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot | RouteState - ): Promise<[string, NavigationExtras]> { - const _replaceValues = CollectionsUtil.isLiteralObject(replaceValues) ? replaceValues : {}; - - return this._extractRouteState(activatedRoute) - .then((state) => this._constructNavigateParameters(state, navigateActionType)) - .then((params) => this._resolveNavigateValues(params.url, params.replacers, _replaceValues, params.navigationExtras)); + return navigateAction.path; + } + + // eslint-disable-next-line max-len + private _appendNavigateExtras( + state: RouteState, + navigationExtras: NavigationExtras, + navigateAction: TaurusNavigateAction, + ): void { + if (CollectionsUtil.isString(navigateAction.queryParamsHandling)) { + navigationExtras.queryParamsHandling = navigateAction.queryParamsHandling; + + if (navigationExtras.queryParamsHandling === "merge") { + navigationExtras.queryParams = { + ...state.queryParams, + ...(navigateAction.queryParams ?? {}), + }; + } else if (navigationExtras.queryParamsHandling !== "preserve") { + navigationExtras.queryParams = navigateAction.queryParams ?? {}; + } + } else if (CollectionsUtil.isNull(navigateAction.queryParamsHandling)) { + delete navigationExtras.queryParamsHandling; + } else { + navigationExtras.queryParams = { + ...state.queryParams, + ...(navigateAction.queryParams ?? {}), + }; } - - /** - * ** Initialize the Service without any operation, just to create singleton Service instance. - * - * - It should be done in the root of the project only once. - * - Possible place is AppComponent or some root initializer guard. - */ - initialize(): void { - this.routerService.initialize(); + } + + private _resolveNavigateValues( + path: string, + replacers: Array>, + replaceValues: { + [key: string]: ArrayElement< + TaurusNavigateAction["replacers"] + >["replaceValue"]; + }, + navigationExtras: NavigationExtras, + ): Promise<[string, NavigationExtras]> { + let resolvedPath: string; + + if (CollectionsUtil.isString(path)) { + resolvedPath = path; + } else { + resolvedPath = "/"; + + console.error( + `RouteConfig error! NavigationAction config, "path" property is missing.`, + ); } - private _navigateWithAction( - replaceValues: { [key: string]: ArrayElement['replaceValue'] }, - activatedRoute: ActivatedRoute | ActivatedRouteSnapshot, - dataKey: 'navigateTo' | 'navigateBack' | 'redirect' - ): Promise { - return this.resolveNavigateActionUrl(dataKey, replaceValues, activatedRoute) - .then() - .then(([url, navigationExtras]) => this.navigate(url, navigationExtras)) - .catch((error) => { - console.error(error); - - return false; - }); - } - - private _extractRouteState(activatedRoute: ActivatedRoute | ActivatedRouteSnapshot | RouteState): Promise { - // NOTICE - // this piece of code is needed for Guards cases usage. - // Use params and data from Guard RouteState instead from RouterService because - // RouterService state is populated after all guards are resolved - if (CollectionsUtil.isDefined(activatedRoute)) { - if (activatedRoute instanceof RouteState) { - return Promise.resolve(activatedRoute); - } - - return Promise.resolve( - this._routeStateFactory.create(activatedRoute instanceof ActivatedRoute ? activatedRoute.snapshot : activatedRoute, null) - ); - } - - return this.routerService - .getState() - .pipe(take(1)) - .toPromise() - .then((state) => state); - } + for (const replacer of replacers) { + const replaceValue = + replaceValues[replacer.replaceValue] ?? replacer.replaceValue; - // eslint-disable-next-line max-len - private _constructNavigateParameters( - state: RouteState, - dataKey: 'navigateTo' | 'navigateBack' | 'redirect' - ): Promise { - const navigateAction = state.getData(dataKey); - const navigationExtras: NavigationExtras = { queryParamsHandling: 'merge' }; - - let url: string; - let replacers: Array> = []; - - if (CollectionsUtil.isDefined(navigateAction)) { - url = this._resolveNavigateUrl(state, dataKey, navigateAction); - - if (CollectionsUtil.isArray(navigateAction.replacers)) { - replacers = navigateAction.replacers; - } - - this._appendNavigateExtras(state, navigationExtras, navigateAction); - } else if (dataKey === 'navigateBack') { - url = state.getParentAbsoluteRoutePath(); - } else { - return Promise.reject('Cannot navigate without NavigationAction'); - } - - return Promise.resolve({ - url, - replacers, - navigationExtras - }); + resolvedPath = resolvedPath.replace(replacer.searchValue, replaceValue); } - private _resolveNavigateUrl( - state: RouteState, - dataKey: 'navigateTo' | 'navigateBack' | 'redirect', - navigateAction: TaurusNavigateAction - ): string { - if (dataKey === 'redirect' || dataKey === 'navigateBack') { - if (navigateAction.path === '$.parent' || navigateAction.path === '$.requested') { - return navigateAction.useConfigPath ? state.getParentAbsoluteConfigPath() : state.getParentAbsoluteRoutePath(); - } - - if (navigateAction.path === '$.current') { - return navigateAction.useConfigPath ? state.getAbsoluteConfigPath() : state.getAbsoluteRoutePath(); - } - - return navigateAction.path; - } - - if (navigateAction.path === '$.current') { - return navigateAction.useConfigPath ? state.getAbsoluteConfigPath() : state.getAbsoluteRoutePath(); - } - - return navigateAction.path; - } - - // eslint-disable-next-line max-len - private _appendNavigateExtras(state: RouteState, navigationExtras: NavigationExtras, navigateAction: TaurusNavigateAction): void { - if (CollectionsUtil.isString(navigateAction.queryParamsHandling)) { - navigationExtras.queryParamsHandling = navigateAction.queryParamsHandling; - - if (navigationExtras.queryParamsHandling === 'merge') { - navigationExtras.queryParams = { - ...state.queryParams, - ...(navigateAction.queryParams ?? {}) - }; - } else if (navigationExtras.queryParamsHandling !== 'preserve') { - navigationExtras.queryParams = navigateAction.queryParams ?? {}; - } - } else if (CollectionsUtil.isNull(navigateAction.queryParamsHandling)) { - delete navigationExtras.queryParamsHandling; - } else { - navigationExtras.queryParams = { - ...state.queryParams, - ...(navigateAction.queryParams ?? {}) - }; - } - } - - private _resolveNavigateValues( - path: string, - replacers: Array>, - replaceValues: { [key: string]: ArrayElement['replaceValue'] }, - navigationExtras: NavigationExtras - ): Promise<[string, NavigationExtras]> { - let resolvedPath: string; - - if (CollectionsUtil.isString(path)) { - resolvedPath = path; - } else { - resolvedPath = '/'; - - console.error(`RouteConfig error! NavigationAction config, "path" property is missing.`); - } - - for (const replacer of replacers) { - const replaceValue = replaceValues[replacer.replaceValue] ?? replacer.replaceValue; - - resolvedPath = resolvedPath.replace(replacer.searchValue, replaceValue); - } - - return Promise.resolve([resolvedPath, navigationExtras]); - } + return Promise.resolve([resolvedPath, navigationExtras]); + } } interface NavigationParameters { - url: string; - replacers: Array>; - navigationExtras: NavigationExtras; + url: string; + replacers: Array>; + navigationExtras: NavigationExtras; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts index 26793518be..c1b9854707 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts @@ -3,81 +3,93 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { BaseAction, BaseActionWithPayload, GenericAction } from './base.actions'; +import { + BaseAction, + BaseActionWithPayload, + GenericAction, +} from "./base.actions"; -describe('BaseActionWithPayload', () => { - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify will throw Error', () => { - // Then - expect(() => BaseActionWithPayload.of(null, null)).toThrowError('Method have to be overridden in Subclasses.'); - }); - }); +describe("BaseActionWithPayload", () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify will throw Error", () => { + // Then + expect(() => BaseActionWithPayload.of(null, null)).toThrowError( + "Method have to be overridden in Subclasses.", + ); }); + }); }); + }); }); -describe('GenericAction', () => { - it('should verify instance is created', () => { - // When - const instance = new GenericAction(null, null); +describe("GenericAction", () => { + it("should verify instance is created", () => { + // When + const instance = new GenericAction(null, null); - // Then - expect(instance).toBeDefined(); - }); + // Then + expect(instance).toBeDefined(); + }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // Given - const type = '[component] Create State'; - const payload = { state: [1, 2, 3, 4] }; + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // Given + const type = "[component] Create State"; + const payload = { state: [1, 2, 3, 4] }; - // When - const instance = GenericAction.of(type, payload); + // When + const instance = GenericAction.of(type, payload); - // Then - expect(instance).toBeInstanceOf(GenericAction); - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); + // Then + expect(instance).toBeInstanceOf(GenericAction); + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); - expect(instance.type).toEqual(type); - expect(instance.payload).toBe(payload); - }); + expect(instance.type).toEqual(type); + expect(instance.payload).toBe(payload); + }); - it('should verify factory method will create instance with Task', () => { - // Given - const type = '[component] Submit Data'; - const payload = { state: [10, 20, 30, 40] }; - const task = 'patch_entity'; - const dateNowISO = new Date().toISOString(); - const taskIdentifier = `${task} __ ${dateNowISO}`; - const dateISOSpy = spyOn(CollectionsUtil, 'dateISO').and.returnValue(dateNowISO); - const interpolateStringSpy = spyOn( - CollectionsUtil, - 'interpolateString' as never - ).and.returnValue(taskIdentifier as never); + it("should verify factory method will create instance with Task", () => { + // Given + const type = "[component] Submit Data"; + const payload = { state: [10, 20, 30, 40] }; + const task = "patch_entity"; + const dateNowISO = new Date().toISOString(); + const taskIdentifier = `${task} __ ${dateNowISO}`; + const dateISOSpy = spyOn(CollectionsUtil, "dateISO").and.returnValue( + dateNowISO, + ); + const interpolateStringSpy = spyOn( + CollectionsUtil, + "interpolateString" as never, + ).and.returnValue(taskIdentifier as never); - // When - const instance = GenericAction.of(type, payload, task); + // When + const instance = GenericAction.of(type, payload, task); - // Then - expect(instance).toBeInstanceOf(GenericAction); - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); + // Then + expect(instance).toBeInstanceOf(GenericAction); + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); - expect(instance.type).toEqual(type); - expect(instance.payload).toBe(payload); - expect(instance.task).toEqual(taskIdentifier); - expect(dateISOSpy).toHaveBeenCalled(); - // @ts-ignore - expect(interpolateStringSpy).toHaveBeenCalledWith('%s __ %s' as never, task as never, dateNowISO as never); - }); - }); + expect(instance.type).toEqual(type); + expect(instance.payload).toBe(payload); + expect(instance.task).toEqual(taskIdentifier); + expect(dateISOSpy).toHaveBeenCalled(); + // @ts-ignore + expect(interpolateStringSpy).toHaveBeenCalledWith( + "%s __ %s" as never, + task as never, + dateNowISO as never, + ); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts index 0818622521..82b1a51fe2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts @@ -3,57 +3,57 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Action } from '@ngrx/store'; +import { Action } from "@ngrx/store"; -import { createTaskIdentifier } from '../../../common'; +import { createTaskIdentifier } from "../../../common"; /** * ** Base Action for Redux Impl. */ export abstract class BaseAction implements Action { - /** - * ** Type of Action. - */ - readonly type: string; + /** + * ** Type of Action. + */ + readonly type: string; - /** - * ** Constructor. - */ - protected constructor(type: string) { - this.type = type; - } + /** + * ** Constructor. + */ + protected constructor(type: string) { + this.type = type; + } } /** * ** Base Action with payload for Redux Impl. */ export abstract class BaseActionWithPayload extends BaseAction { - /** - * ** Action payload data. - */ - readonly payload: T; + /** + * ** Action payload data. + */ + readonly payload: T; - /** - * ** Action Task. - */ - readonly task: string; + /** + * ** Action Task. + */ + readonly task: string; - /** - * ** Constructor. - */ - protected constructor(type: string, payload: T, task?: string) { - super(type); + /** + * ** Constructor. + */ + protected constructor(type: string, payload: T, task?: string) { + super(type); - this.payload = payload; - this.task = createTaskIdentifier(task); - } + this.payload = payload; + this.task = createTaskIdentifier(task); + } - /** - * ** Factory method that have to be overridden in Subclasses. - */ - static of(..._args: unknown[]): BaseActionWithPayload { - throw new Error('Method have to be overridden in Subclasses.'); - } + /** + * ** Factory method that have to be overridden in Subclasses. + */ + static of(..._args: unknown[]): BaseActionWithPayload { + throw new Error("Method have to be overridden in Subclasses."); + } } /** @@ -62,17 +62,21 @@ export abstract class BaseActionWithPayload extends BaseAction { * - All parameters type, payload and optional task are provided to Constructor. */ export class GenericAction extends BaseActionWithPayload { - /** - * ** Constructor. - */ - constructor(type: string, payload: T, task?: string) { - super(type, payload, task); - } + /** + * ** Constructor. + */ + constructor(type: string, payload: T, task?: string) { + super(type, payload, task); + } - /** - * ** Factory method for Generic Action with type and payload in Constructor. - */ - static override of(type: string, payload: K, task?: string): GenericAction { - return new GenericAction(type, payload, task); - } + /** + * ** Factory method for Generic Action with type and payload in Constructor. + */ + static override of( + type: string, + payload: K, + task?: string, + ): GenericAction { + return new GenericAction(type, payload, task); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts index 30aec26079..23b459511a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './base.actions'; +export * from "./base.actions"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts index 8b19555f46..8d343446ac 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './ngrx-config.model'; +export * from "./ngrx-config.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts index 9e78b68a15..04b2b7fd76 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts @@ -3,28 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NGRX_STORE_CONFIG, NGRX_STORE_DEVTOOLS_CONFIG } from './ngrx-config.model'; +import { + NGRX_STORE_CONFIG, + NGRX_STORE_DEVTOOLS_CONFIG, +} from "./ngrx-config.model"; -describe('NGRX_STORE_DEVTOOLS_CONFIG', () => { - it('should verify default values are set', () => { - // Then - expect(NGRX_STORE_DEVTOOLS_CONFIG).toEqual({ - maxAge: 100, - serialize: true, - logOnly: false, - name: 'Taurus NgRx Store' - }); +describe("NGRX_STORE_DEVTOOLS_CONFIG", () => { + it("should verify default values are set", () => { + // Then + expect(NGRX_STORE_DEVTOOLS_CONFIG).toEqual({ + maxAge: 100, + serialize: true, + logOnly: false, + name: "Taurus NgRx Store", }); + }); }); -describe('NGRX_STORE_CONFIG', () => { - it('should verify default values are set', () => { - // Then - expect(NGRX_STORE_CONFIG).toEqual({ - runtimeChecks: { - strictActionImmutability: true, - strictStateImmutability: true - } - }); +describe("NGRX_STORE_CONFIG", () => { + it("should verify default values are set", () => { + // Then + expect(NGRX_STORE_CONFIG).toEqual({ + runtimeChecks: { + strictActionImmutability: true, + strictStateImmutability: true, + }, }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts index 63dfc6c2e6..d2c08b4da2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RootStoreConfig } from '@ngrx/store'; -import { StoreDevtoolsConfig } from '@ngrx/store-devtools'; +import { RootStoreConfig } from "@ngrx/store"; +import { StoreDevtoolsConfig } from "@ngrx/store-devtools"; /** * ** Configuration for NgRx Store Devtools. */ export const NGRX_STORE_DEVTOOLS_CONFIG: StoreDevtoolsConfig = { - maxAge: 100, - serialize: true, - logOnly: false, - name: 'Taurus NgRx Store' + maxAge: 100, + serialize: true, + logOnly: false, + name: "Taurus NgRx Store", }; /** @@ -21,19 +21,19 @@ export const NGRX_STORE_DEVTOOLS_CONFIG: StoreDevtoolsConfig = { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const NGRX_STORE_CONFIG: RootStoreConfig = { - runtimeChecks: { - strictActionImmutability: true, - strictStateImmutability: true - } + runtimeChecks: { + strictActionImmutability: true, + strictStateImmutability: true, + }, }; /** * ** Config for Taurus impl of NgRx. */ export interface TaurusNgRxConfig { - /** - * ** StoreDevTools configuration replica. - *

    see {@link https://ngrx.io/guide/store-devtools/config}

    - */ - storeDevToolsConfig?: StoreDevtoolsConfig; + /** + * ** StoreDevTools configuration replica. + *

    see {@link https://ngrx.io/guide/store-devtools/config}

    + */ + storeDevToolsConfig?: StoreDevtoolsConfig; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts index 5b1b453a19..e0741ad991 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './root-effects.registry'; +export * from "./root-effects.registry"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/index.ts index e5b0ad569e..465844ca7a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './taurus-base.effects'; +export * from "./taurus-base.effects"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.spec.ts index 0fac2ae968..9441009f41 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.spec.ts @@ -3,94 +3,101 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive, Injectable } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { Directive, Injectable } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; -import { Subject } from 'rxjs'; +import { Subject } from "rxjs"; -import { Actions } from '@ngrx/effects'; +import { Actions } from "@ngrx/effects"; -import { provideMockActions } from '@ngrx/effects/testing'; +import { provideMockActions } from "@ngrx/effects/testing"; -import { TaurusObject } from '../../../../common'; +import { TaurusObject } from "../../../../common"; -import { ComponentService } from '../../../component'; +import { ComponentService } from "../../../component"; -import { TaurusBaseEffects } from './taurus-base.effects'; +import { TaurusBaseEffects } from "./taurus-base.effects"; @Injectable() class TaurusBaseEffectsStub extends TaurusBaseEffects { - static override readonly CLASS_NAME: string = 'TaurusBaseEffectsStub'; + static override readonly CLASS_NAME: string = "TaurusBaseEffectsStub"; - constructor(actions$: Actions, componentService: ComponentService) { - super(actions$, componentService, TaurusBaseEffectsStub.CLASS_NAME); - } + constructor(actions$: Actions, componentService: ComponentService) { + super(actions$, componentService, TaurusBaseEffectsStub.CLASS_NAME); + } - protected registerEffectsErrorCodes(): void { - // No-op. - } + protected registerEffectsErrorCodes(): void { + // No-op. + } } @Directive() class TaurusBaseEffectsStubV1 extends TaurusBaseEffects { - constructor(actions$: Actions, componentService: ComponentService, className: string) { - super(actions$, componentService, className); - } - - protected registerEffectsErrorCodes(): void { - // No-op. - } + constructor( + actions$: Actions, + componentService: ComponentService, + className: string, + ) { + super(actions$, componentService, className); + } + + protected registerEffectsErrorCodes(): void { + // No-op. + } } -describe('TaurusBaseEffects', () => { - let componentServiceStub: jasmine.SpyObj; - - let effectService: TaurusBaseEffects; +describe("TaurusBaseEffects", () => { + let componentServiceStub: jasmine.SpyObj; - beforeEach(() => { - componentServiceStub = jasmine.createSpyObj('componentServiceStub', ['load']); + let effectService: TaurusBaseEffects; - TestBed.configureTestingModule({ - providers: [ - TaurusBaseEffectsStub, - { provide: ComponentService, useValue: componentServiceStub }, - provideMockActions(() => new Subject()) - ] - }); + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj( + "componentServiceStub", + ["load"], + ); - effectService = TestBed.inject(TaurusBaseEffectsStub); + TestBed.configureTestingModule({ + providers: [ + TaurusBaseEffectsStub, + { provide: ComponentService, useValue: componentServiceStub }, + provideMockActions(() => new Subject()), + ], }); - it('should verify effect class is created', () => { - // When - // @ts-ignore - const effectService1 = new TaurusBaseEffectsStubV1(null, null, null); - + effectService = TestBed.inject(TaurusBaseEffectsStub); + }); + + it("should verify effect class is created", () => { + // When + // @ts-ignore + const effectService1 = new TaurusBaseEffectsStubV1(null, null, null); + + // Then + expect(effectService).toBeDefined(); + expect(effectService).toBeInstanceOf(TaurusBaseEffectsStub); + expect(effectService).toBeInstanceOf(TaurusBaseEffects); + expect(effectService).toBeInstanceOf(TaurusObject); + + expect(effectService1).toBeDefined(); + expect(effectService1).toBeInstanceOf(TaurusBaseEffectsStubV1); + expect(effectService1).toBeInstanceOf(TaurusBaseEffects); + expect(effectService1).toBeInstanceOf(TaurusObject); + }); + + describe("Statics::", () => { + describe("|CLASS_NAME|", () => { + it("should verify value", () => { // Then - expect(effectService).toBeDefined(); - expect(effectService).toBeInstanceOf(TaurusBaseEffectsStub); - expect(effectService).toBeInstanceOf(TaurusBaseEffects); - expect(effectService).toBeInstanceOf(TaurusObject); - - expect(effectService1).toBeDefined(); - expect(effectService1).toBeInstanceOf(TaurusBaseEffectsStubV1); - expect(effectService1).toBeInstanceOf(TaurusBaseEffects); - expect(effectService1).toBeInstanceOf(TaurusObject); + expect(TaurusBaseEffects.CLASS_NAME).toEqual("TaurusBaseEffects"); + }); }); - describe('Statics::', () => { - describe('|CLASS_NAME|', () => { - it('should verify value', () => { - // Then - expect(TaurusBaseEffects.CLASS_NAME).toEqual('TaurusBaseEffects'); - }); - }); - - describe('|PUBLIC_NAME|', () => { - it('should verify value', () => { - // Then - expect(TaurusBaseEffects.PUBLIC_NAME).toEqual('Taurus-Base-Effects'); - }); - }); + describe("|PUBLIC_NAME|", () => { + it("should verify value", () => { + // Then + expect(TaurusBaseEffects.PUBLIC_NAME).toEqual("Taurus-Base-Effects"); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.ts index 66ac57de7d..3cbfc50795 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/model/taurus-base.effects.ts @@ -3,49 +3,49 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive } from '@angular/core'; +import { Directive } from "@angular/core"; -import { Actions } from '@ngrx/effects'; +import { Actions } from "@ngrx/effects"; -import { TaurusObject } from '../../../../common'; +import { TaurusObject } from "../../../../common"; -import { ComponentService } from '../../../component'; +import { ComponentService } from "../../../component"; /** * ** Base class for all NgRx Effects. */ @Directive() export abstract class TaurusBaseEffects extends TaurusObject { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'TaurusBaseEffects'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Taurus-Base-Effects'; - - /** - * ** Constructor. - * - * @protected - */ - protected constructor( - protected readonly actions$: Actions, - protected readonly componentService: ComponentService, - className?: string - ) { - super(className ?? TaurusBaseEffects.CLASS_NAME); - } - - /** - * ** Implement this method and register all error codes that could be recorded from Class effects. - * - * - Bound error codes to error-codes repository when keys are tasks name and value is all available error codes for that particular task. - * - Implement in subclasses and invoke in Constructor to register Effects Error Codes. - * - * @protected - */ - protected abstract registerEffectsErrorCodes(): void; + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "TaurusBaseEffects"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Taurus-Base-Effects"; + + /** + * ** Constructor. + * + * @protected + */ + protected constructor( + protected readonly actions$: Actions, + protected readonly componentService: ComponentService, + className?: string, + ) { + super(className ?? TaurusBaseEffects.CLASS_NAME); + } + + /** + * ** Implement this method and register all error codes that could be recorded from Class effects. + * + * - Bound error codes to error-codes repository when keys are tasks name and value is all available error codes for that particular task. + * - Implement in subclasses and invoke in Constructor to register Effects Error Codes. + * + * @protected + */ + protected abstract registerEffectsErrorCodes(): void; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts index f60a9ca5ee..90c41edf47 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RouterEffects } from '../../router/state/effects'; +import { RouterEffects } from "../../router/state/effects"; -import { SHARED_ROOT_EFFECTS } from './root-effects.registry'; +import { SHARED_ROOT_EFFECTS } from "./root-effects.registry"; -describe('SHARED_ROOT_EFFECTS', () => { - it('should verify expected Effects are in this registry', () => { - // Then - expect(SHARED_ROOT_EFFECTS).toEqual([RouterEffects]); - }); +describe("SHARED_ROOT_EFFECTS", () => { + it("should verify expected Effects are in this registry", () => { + // Then + expect(SHARED_ROOT_EFFECTS).toEqual([RouterEffects]); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts index 73946c6416..d9cd73e6dc 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Type } from '@angular/core'; +import { Type } from "@angular/core"; -import { RouterEffects } from '../../router/state/effects'; +import { RouterEffects } from "../../router/state/effects"; /** * ** Registry for Root Effects. diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts index 998483c7e6..c25e6f59ce 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './vdk-shared-ngrx-dev.module'; -export * from './vdk-shared-ngrx-prod.module'; +export * from "./vdk-shared-ngrx-dev.module"; +export * from "./vdk-shared-ngrx-prod.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-dev.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-dev.module.ts index 9899e8ccd8..34f372fe35 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-dev.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-dev.module.ts @@ -3,54 +3,64 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ModuleWithProviders, NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from "@angular/core"; -import { StoreModule } from '@ngrx/store'; -import { StoreRouterConnectingModule } from '@ngrx/router-store'; -import { EffectsModule } from '@ngrx/effects'; -import { StoreDevtoolsConfig, StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { StoreModule } from "@ngrx/store"; +import { StoreRouterConnectingModule } from "@ngrx/router-store"; +import { EffectsModule } from "@ngrx/effects"; +import { StoreDevtoolsConfig, StoreDevtoolsModule } from "@ngrx/store-devtools"; -import { RouterService, RouterServiceImpl } from '../../router'; -import { SharedRouterSerializer } from '../../router/integration/ngrx'; +import { RouterService, RouterServiceImpl } from "../../router"; +import { SharedRouterSerializer } from "../../router/integration/ngrx"; -import { ComponentService, ComponentServiceImpl } from '../../component'; +import { ComponentService, ComponentServiceImpl } from "../../component"; -import { SHARED_ROOT_REDUCERS } from '../reducers'; -import { SHARED_ROOT_EFFECTS } from '../effects'; -import { NGRX_STORE_CONFIG, NGRX_STORE_DEVTOOLS_CONFIG } from '../config'; -import { STORE_ROUTER } from '../state'; +import { SHARED_ROOT_REDUCERS } from "../reducers"; +import { SHARED_ROOT_EFFECTS } from "../effects"; +import { NGRX_STORE_CONFIG, NGRX_STORE_DEVTOOLS_CONFIG } from "../config"; +import { STORE_ROUTER } from "../state"; /** * ** VDK NgRx Redux module recommended for use in Development builds. */ @NgModule({ - imports: [ - StoreModule.forRoot(SHARED_ROOT_REDUCERS, NGRX_STORE_CONFIG), - EffectsModule.forRoot(SHARED_ROOT_EFFECTS), - StoreDevtoolsModule.instrument(() => VdkSharedNgrxDevModule.storeDevToolsConfig), - StoreRouterConnectingModule.forRoot({ - stateKey: STORE_ROUTER, - serializer: SharedRouterSerializer - }) - ], - exports: [StoreModule, EffectsModule, StoreDevtoolsModule, StoreRouterConnectingModule] + imports: [ + StoreModule.forRoot(SHARED_ROOT_REDUCERS, NGRX_STORE_CONFIG), + EffectsModule.forRoot(SHARED_ROOT_EFFECTS), + StoreDevtoolsModule.instrument( + () => VdkSharedNgrxDevModule.storeDevToolsConfig, + ), + StoreRouterConnectingModule.forRoot({ + stateKey: STORE_ROUTER, + serializer: SharedRouterSerializer, + }), + ], + exports: [ + StoreModule, + EffectsModule, + StoreDevtoolsModule, + StoreRouterConnectingModule, + ], }) export class VdkSharedNgrxDevModule { - private static storeDevToolsConfig: StoreDevtoolsConfig = NGRX_STORE_DEVTOOLS_CONFIG; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any - static forRoot(config: StoreDevtoolsConfig = {} as any): ModuleWithProviders { - VdkSharedNgrxDevModule.storeDevToolsConfig = { - ...NGRX_STORE_DEVTOOLS_CONFIG, - ...config - }; - - return { - ngModule: VdkSharedNgrxDevModule, - providers: [ - { provide: RouterService, useClass: RouterServiceImpl }, - { provide: ComponentService, useClass: ComponentServiceImpl } - ] - }; - } + private static storeDevToolsConfig: StoreDevtoolsConfig = + NGRX_STORE_DEVTOOLS_CONFIG; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any + static forRoot( + config: StoreDevtoolsConfig = {} as any, + ): ModuleWithProviders { + VdkSharedNgrxDevModule.storeDevToolsConfig = { + ...NGRX_STORE_DEVTOOLS_CONFIG, + ...config, + }; + + return { + ngModule: VdkSharedNgrxDevModule, + providers: [ + { provide: RouterService, useClass: RouterServiceImpl }, + { provide: ComponentService, useClass: ComponentServiceImpl }, + ], + }; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-prod.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-prod.module.ts index c67625391c..e131a9c42a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-prod.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/vdk-shared-ngrx-prod.module.ts @@ -3,44 +3,44 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ModuleWithProviders, NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from "@angular/core"; -import { StoreModule } from '@ngrx/store'; -import { StoreRouterConnectingModule } from '@ngrx/router-store'; -import { EffectsModule } from '@ngrx/effects'; +import { StoreModule } from "@ngrx/store"; +import { StoreRouterConnectingModule } from "@ngrx/router-store"; +import { EffectsModule } from "@ngrx/effects"; -import { RouterService, RouterServiceImpl } from '../../router'; -import { SharedRouterSerializer } from '../../router/integration/ngrx'; +import { RouterService, RouterServiceImpl } from "../../router"; +import { SharedRouterSerializer } from "../../router/integration/ngrx"; -import { ComponentService, ComponentServiceImpl } from '../../component'; +import { ComponentService, ComponentServiceImpl } from "../../component"; -import { SHARED_ROOT_REDUCERS } from '../reducers'; -import { SHARED_ROOT_EFFECTS } from '../effects'; -import { NGRX_STORE_CONFIG } from '../config'; -import { STORE_ROUTER } from '../state'; +import { SHARED_ROOT_REDUCERS } from "../reducers"; +import { SHARED_ROOT_EFFECTS } from "../effects"; +import { NGRX_STORE_CONFIG } from "../config"; +import { STORE_ROUTER } from "../state"; /** * ** VDK NgRx Redux module recommended for use in Production builds. */ @NgModule({ - imports: [ - StoreModule.forRoot(SHARED_ROOT_REDUCERS, NGRX_STORE_CONFIG), - EffectsModule.forRoot(SHARED_ROOT_EFFECTS), - StoreRouterConnectingModule.forRoot({ - stateKey: STORE_ROUTER, - serializer: SharedRouterSerializer - }) - ], - exports: [StoreModule, EffectsModule, StoreRouterConnectingModule] + imports: [ + StoreModule.forRoot(SHARED_ROOT_REDUCERS, NGRX_STORE_CONFIG), + EffectsModule.forRoot(SHARED_ROOT_EFFECTS), + StoreRouterConnectingModule.forRoot({ + stateKey: STORE_ROUTER, + serializer: SharedRouterSerializer, + }), + ], + exports: [StoreModule, EffectsModule, StoreRouterConnectingModule], }) export class VdkSharedNgrxProdModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: VdkSharedNgrxProdModule, - providers: [ - { provide: RouterService, useClass: RouterServiceImpl }, - { provide: ComponentService, useClass: ComponentServiceImpl } - ] - }; - } + static forRoot(): ModuleWithProviders { + return { + ngModule: VdkSharedNgrxProdModule, + providers: [ + { provide: RouterService, useClass: RouterServiceImpl }, + { provide: ComponentService, useClass: ComponentServiceImpl }, + ], + }; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts index 33d5cb179c..0b459d3ff9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './actions'; -export * from './effects'; -export * from './state'; +export * from "./actions"; +export * from "./effects"; +export * from "./state"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts index 84a1cfd8f5..860f89e7fd 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './actions'; -export * from './effects/model'; -export * from './state'; +export * from "./actions"; +export * from "./effects/model"; +export * from "./state"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts index b663741452..cc8559f39b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './shared-root-reducers.registry'; +export * from "./shared-root-reducers.registry"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts index 275080a015..0723d7abc4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts @@ -3,20 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { routerReducer } from '../../router/state/reducers'; +import { routerReducer } from "../../router/state/reducers"; -import { componentReducer } from '../../component'; +import { componentReducer } from "../../component"; -import { STORE_COMPONENTS, STORE_ROUTER } from '../state'; +import { STORE_COMPONENTS, STORE_ROUTER } from "../state"; -import { SHARED_ROOT_REDUCERS } from './shared-root-reducers.registry'; +import { SHARED_ROOT_REDUCERS } from "./shared-root-reducers.registry"; -describe('SHARED_ROOT_REDUCERS', () => { - it('should verify root reducers are registered', () => { - // Then - expect(SHARED_ROOT_REDUCERS).toEqual({ - [STORE_ROUTER]: routerReducer, - [STORE_COMPONENTS]: componentReducer - }); +describe("SHARED_ROOT_REDUCERS", () => { + it("should verify root reducers are registered", () => { + // Then + expect(SHARED_ROOT_REDUCERS).toEqual({ + [STORE_ROUTER]: routerReducer, + [STORE_COMPONENTS]: componentReducer, }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts index 7398826a3e..285424a4f4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ActionReducerMap } from '@ngrx/store'; +import { ActionReducerMap } from "@ngrx/store"; -import { STORE_COMPONENTS, STORE_ROUTER, StoreState } from '../state'; +import { STORE_COMPONENTS, STORE_ROUTER, StoreState } from "../state"; -import { routerReducer } from '../../router/state/reducers'; -import { componentReducer } from '../../component/state'; +import { routerReducer } from "../../router/state/reducers"; +import { componentReducer } from "../../component/state"; /** * ** Root reducers for Shared. */ export const SHARED_ROOT_REDUCERS: ActionReducerMap = { - [STORE_ROUTER]: routerReducer, - [STORE_COMPONENTS]: componentReducer + [STORE_ROUTER]: routerReducer, + [STORE_COMPONENTS]: componentReducer, }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts index 5f12323fc9..f4f13a53c1 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './store-state.model'; +export * from "./store-state.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts index 30a5a0f5b6..b316eb0bd7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts @@ -3,23 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RouterState } from '../../router'; -import { LiteralComponentsState } from '../../component'; +import { RouterState } from "../../router"; +import { LiteralComponentsState } from "../../component"; /** * ** Constant for Store Router field. */ -export const STORE_ROUTER = 'router'; +export const STORE_ROUTER = "router"; /** * ** Constant for Store Components field. */ -export const STORE_COMPONENTS = 'components'; +export const STORE_COMPONENTS = "components"; /** * ** Store State interface. */ export interface StoreState { - [STORE_ROUTER]: RouterState; - [STORE_COMPONENTS]: LiteralComponentsState; + [STORE_ROUTER]: RouterState; + [STORE_COMPONENTS]: LiteralComponentsState; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/vdk-shared-ngrx.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/vdk-shared-ngrx.module.ts index 4370dc8449..8b89cdc531 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/vdk-shared-ngrx.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/vdk-shared-ngrx.module.ts @@ -5,75 +5,94 @@ /* eslint-disable @typescript-eslint/unified-signatures */ -import { InjectionToken, ModuleWithProviders, NgModule, Type } from '@angular/core'; +import { + InjectionToken, + ModuleWithProviders, + NgModule, + Type, +} from "@angular/core"; -import { Action, ActionReducer, ActionReducerMap, StoreConfig, StoreFeatureModule, StoreModule } from '@ngrx/store'; -import { EffectsFeatureModule, EffectsModule } from '@ngrx/effects'; +import { + Action, + ActionReducer, + ActionReducerMap, + StoreConfig, + StoreFeatureModule, + StoreModule, +} from "@ngrx/store"; +import { EffectsFeatureModule, EffectsModule } from "@ngrx/effects"; -import { TaurusNgRxConfig } from './config'; -import { VdkSharedNgrxProdModule, VdkSharedNgrxDevModule } from './helper-modules'; +import { TaurusNgRxConfig } from "./config"; +import { + VdkSharedNgrxProdModule, + VdkSharedNgrxDevModule, +} from "./helper-modules"; /** * ** Integration Class module for NgRx Redux. */ @NgModule({}) export class VdkSharedNgRxModule { - /** - * ** Provides VdkSharedNgrxProdModule and all Services related to VDK Redux. - * - * - Recommended for Prod (release) builds. - * - Should be invoked at Root and only once for entire project. - * - In FeaturesModules (lazy loaded Modules) invoke one of the methods forFeatureEffects/forFeatureStore. - */ - static forRoot(): ModuleWithProviders { - return VdkSharedNgrxProdModule.forRoot(); - } + /** + * ** Provides VdkSharedNgrxProdModule and all Services related to VDK Redux. + * + * - Recommended for Prod (release) builds. + * - Should be invoked at Root and only once for entire project. + * - In FeaturesModules (lazy loaded Modules) invoke one of the methods forFeatureEffects/forFeatureStore. + */ + static forRoot(): ModuleWithProviders { + return VdkSharedNgrxProdModule.forRoot(); + } - /** - * ** Provides VdkSharedNgrxDevModule including StoreDevTools and all Services related to VDK Redux. - * - * - Recommended for Dev (local) builds. - * - Should be invoked at Root and only once for entire project. - * - In FeaturesModules (lazy loaded Modules) invoke one of the methods forFeatureEffects/forFeatureStore. - */ - static forRootWithDevtools(config: TaurusNgRxConfig = {}): ModuleWithProviders { - return VdkSharedNgrxDevModule.forRoot(config.storeDevToolsConfig); - } + /** + * ** Provides VdkSharedNgrxDevModule including StoreDevTools and all Services related to VDK Redux. + * + * - Recommended for Dev (local) builds. + * - Should be invoked at Root and only once for entire project. + * - In FeaturesModules (lazy loaded Modules) invoke one of the methods forFeatureEffects/forFeatureStore. + */ + static forRootWithDevtools( + config: TaurusNgRxConfig = {}, + ): ModuleWithProviders { + return VdkSharedNgrxDevModule.forRoot(config.storeDevToolsConfig); + } - /** - * ** Load Features Effects. - * - * - Should be invoke in FeatureModules (lazy loaded Modules). - * - It will register Effects for that Feature. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static forFeatureEffects(effects: Array>): ModuleWithProviders { - return EffectsModule.forFeature(effects); - } + /** + * ** Load Features Effects. + * + * - Should be invoke in FeatureModules (lazy loaded Modules). + * - It will register Effects for that Feature. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static forFeatureEffects( + effects: Array>, + ): ModuleWithProviders { + return EffectsModule.forFeature(effects); + } - /** - * ** Load Features Store reducers. - * - * - Should be invoke in FeatureModules (lazy loaded Modules). - * - It will extend Store and add reducers for that Feature. - */ - static forFeatureStore( - featureName: string, - reducers: ActionReducerMap | InjectionToken>, - config?: StoreConfig | InjectionToken> - ): ModuleWithProviders; - static forFeatureStore( - featureName: string, - reducer: ActionReducer | InjectionToken>, - config?: StoreConfig | InjectionToken> - ): ModuleWithProviders; - static forFeatureStore( - featureName: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - reducer: any, - config?: StoreConfig | InjectionToken> - ): ModuleWithProviders { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return StoreModule.forFeature(featureName, reducer, config); - } + /** + * ** Load Features Store reducers. + * + * - Should be invoke in FeatureModules (lazy loaded Modules). + * - It will extend Store and add reducers for that Feature. + */ + static forFeatureStore( + featureName: string, + reducers: ActionReducerMap | InjectionToken>, + config?: StoreConfig | InjectionToken>, + ): ModuleWithProviders; + static forFeatureStore( + featureName: string, + reducer: ActionReducer | InjectionToken>, + config?: StoreConfig | InjectionToken>, + ): ModuleWithProviders; + static forFeatureStore( + featureName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reducer: any, + config?: StoreConfig | InjectionToken>, + ): ModuleWithProviders { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return StoreModule.forFeature(featureName, reducer, config); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts index 842a0536dd..58d6ccc686 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './component/public-api'; -export * from './error/public-api'; -export * from './navigation/public-api'; -export * from './ngrx/public-api'; -export * from './router/public-api'; -export * from './system-events/public-api'; -export * from './url-state-manager/public-api'; +export * from "./component/public-api"; +export * from "./error/public-api"; +export * from "./navigation/public-api"; +export * from "./ngrx/public-api"; +export * from "./router/public-api"; +export * from "./system-events/public-api"; +export * from "./url-state-manager/public-api"; // export Core module -export * from './vdk-shared-core.module'; +export * from "./vdk-shared-core.module"; // export NgRx modules -export * from './ngrx/vdk-shared-ngrx.module'; -export * from './ngrx/helper-modules'; +export * from "./ngrx/vdk-shared-ngrx.module"; +export * from "./ngrx/helper-modules"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts index f270403bfe..082671e70d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './route-state.factory'; +export * from "./route-state.factory"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts index 2ddaf2abad..109b59165a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts @@ -3,45 +3,55 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { createRouteSnapshot } from '../../../unit-testing'; +import { createRouteSnapshot } from "../../../unit-testing"; + +import { RouteSegments, RouteState } from "../model"; + +import { RouteStateFactory } from "./route-state.factory"; + +describe("RouteStateFactory", () => { + it("should verify instance is created", () => { + // When + const instance = new RouteStateFactory(); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Methods::", () => { + describe("|create|", () => { + it("should verify will return RouteState from provided ActivatedRouteSnapshot", () => { + // Given + const routeSnapshot = createRouteSnapshot({}); + const factory = new RouteStateFactory(); + const assertion = RouteState.of( + new RouteSegments( + "entity/23", + { paramKey: "prime" }, + { entityId: 23 }, + { search: "test-team" }, + new RouteSegments( + "domain/context", + {}, + {}, + {}, + undefined, + "domain/context", + ), + "entity/23", + ), + "domain/context/entity/23", + ); -import { RouteSegments, RouteState } from '../model'; - -import { RouteStateFactory } from './route-state.factory'; - -describe('RouteStateFactory', () => { - it('should verify instance is created', () => { // When - const instance = new RouteStateFactory(); + const routeState = factory.create( + routeSnapshot, + "domain/context/entity/23", + ); // Then - expect(instance).toBeDefined(); - }); - - describe('Methods::', () => { - describe('|create|', () => { - it('should verify will return RouteState from provided ActivatedRouteSnapshot', () => { - // Given - const routeSnapshot = createRouteSnapshot({}); - const factory = new RouteStateFactory(); - const assertion = RouteState.of( - new RouteSegments( - 'entity/23', - { paramKey: 'prime' }, - { entityId: 23 }, - { search: 'test-team' }, - new RouteSegments('domain/context', {}, {}, {}, undefined, 'domain/context'), - 'entity/23' - ), - 'domain/context/entity/23' - ); - - // When - const routeState = factory.create(routeSnapshot, 'domain/context/entity/23'); - - // Then - expect(routeState).toEqual(assertion); - }); - }); + expect(routeState).toEqual(assertion); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts index acd926c98f..542bae0098 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts @@ -3,45 +3,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ActivatedRouteSnapshot, UrlSegment } from '@angular/router'; +import { ActivatedRouteSnapshot, UrlSegment } from "@angular/router"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { RouteSegments, RouteState } from '../model'; +import { RouteSegments, RouteState } from "../model"; /** * ** Route State Factory. */ export class RouteStateFactory { - /** - * ** Creates Router State from provided Route snapshot. - */ - create(routeSnapshot: ActivatedRouteSnapshot, url: string): RouteState { - return RouteState.of(this._getRouteSegments(routeSnapshot), url); + /** + * ** Creates Router State from provided Route snapshot. + */ + create(routeSnapshot: ActivatedRouteSnapshot, url: string): RouteState { + return RouteState.of(this._getRouteSegments(routeSnapshot), url); + } + + private _getRouteSegments( + routeSnapshot: ActivatedRouteSnapshot, + ): RouteSegments { + if (CollectionsUtil.isNil(routeSnapshot)) { + return null; } - private _getRouteSegments(routeSnapshot: ActivatedRouteSnapshot): RouteSegments { - if (CollectionsUtil.isNil(routeSnapshot)) { - return null; - } + const routePathSegments: string[] = []; + routeSnapshot.url.forEach((segment: UrlSegment) => { + routePathSegments.push(segment.path); + }); - const routePathSegments: string[] = []; - routeSnapshot.url.forEach((segment: UrlSegment) => { - routePathSegments.push(segment.path); - }); + const routePath = routePathSegments.join("/"); + const data = CollectionsUtil.cloneDeep(routeSnapshot.data); + const params = CollectionsUtil.cloneDeep(routeSnapshot.params); + const queryParams = CollectionsUtil.cloneDeep(routeSnapshot.queryParams); + const configPath = routeSnapshot.routeConfig?.path; - const routePath = routePathSegments.join('/'); - const data = CollectionsUtil.cloneDeep(routeSnapshot.data); - const params = CollectionsUtil.cloneDeep(routeSnapshot.params); - const queryParams = CollectionsUtil.cloneDeep(routeSnapshot.queryParams); - const configPath = routeSnapshot.routeConfig?.path; + let parentNavSegments: RouteSegments; - let parentNavSegments: RouteSegments; - - if (routeSnapshot.parent) { - parentNavSegments = this._getRouteSegments(routeSnapshot.parent); - } - - return RouteSegments.of(routePath, data, params, queryParams, parentNavSegments, configPath); + if (routeSnapshot.parent) { + parentNavSegments = this._getRouteSegments(routeSnapshot.parent); } + + return RouteSegments.of( + routePath, + data, + params, + queryParams, + parentNavSegments, + configPath, + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts index 2ee552e56a..f13b23279a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './model'; -export * from './state/actions'; -export * from './services'; +export * from "./model"; +export * from "./state/actions"; +export * from "./services"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts index 4f8ee04efc..f179262153 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './router-serializer.service'; +export * from "./router-serializer.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts index e8837828dc..6b7cbb4c3b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts @@ -3,95 +3,130 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { UrlSegment } from '@angular/router'; -import { TestBed } from '@angular/core/testing'; +import { UrlSegment } from "@angular/router"; +import { TestBed } from "@angular/core/testing"; -import { ComponentStub, createRouteSnapshot, RouteConfigStub } from '../../../../unit-testing'; +import { + ComponentStub, + createRouteSnapshot, + RouteConfigStub, +} from "../../../../unit-testing"; -import { RouteSegments, RouteState } from '../../model'; -import { RouteStateFactory } from '../../factory'; +import { RouteSegments, RouteState } from "../../model"; +import { RouteStateFactory } from "../../factory"; -import { SharedRouterSerializer } from './router-serializer.service'; +import { SharedRouterSerializer } from "./router-serializer.service"; -describe('SharedRouterSerializer', () => { - let service: SharedRouterSerializer; +describe("SharedRouterSerializer", () => { + let service: SharedRouterSerializer; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [SharedRouterSerializer] - }); - - service = TestBed.inject(SharedRouterSerializer); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [SharedRouterSerializer], }); - it('should verify instance is created', () => { - // Then - expect(service).toBeDefined(); - }); + service = TestBed.inject(SharedRouterSerializer); + }); - describe('Methods::', () => { - describe('|serialize|', () => { - it('should verify will invoke correct methods', () => { - // Given - const routeState = RouteState.of( - RouteSegments.of( - 'delivery', - { paramKey: 'prime' }, - { entityId: 25 }, - { search: 'test-team' }, - RouteSegments.of( - 'entity/25', - { deferredLoading: true }, - { entityId: 25 }, - { search: 'test-team' }, - RouteSegments.of('domain/context', { entityKey: 'second' }, {}, { search: 'test-team' }, null) - ) - ), - 'domain/context/entity/25/delivery' - ); - const factoryCreateSpy = spyOn(RouteStateFactory.prototype, 'create').and.returnValue(routeState); + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + }); - const routeSnapshotChild2 = createRouteSnapshot({ - url: [new UrlSegment('domain/context', {}), new UrlSegment('entity/25', {}), new UrlSegment('delivery', {})], - data: { entityKey: 'second' }, - params: { entityId: 25 }, - queryParams: { search: 'team-test' }, - outlet: 'router-outlet', - component: ComponentStub, - routeConfig: new RouteConfigStub('delivery', ComponentStub, 'router-outlet') - }); - const routeSnapshotChild1 = createRouteSnapshot({ - url: [new UrlSegment('domain/context', {}), new UrlSegment('entity/25', {})], - data: { deferredLoading: true }, - params: { entityId: 25 }, - queryParams: { search: 'team-test' }, - outlet: 'router-outlet', - component: ComponentStub, - routeConfig: new RouteConfigStub('entity/25', ComponentStub, 'router-outlet'), - firstChild: routeSnapshotChild2 - }); - const rootRouteSnapshot = createRouteSnapshot({ - url: [new UrlSegment('domain/context', {})], - data: { paramKey: 'prime' }, - params: {}, - queryParams: { search: 'team-test' }, - outlet: 'router-outlet', - component: ComponentStub, - routeConfig: new RouteConfigStub('domain/context', ComponentStub, 'router-outlet'), - firstChild: routeSnapshotChild1 - }); + describe("Methods::", () => { + describe("|serialize|", () => { + it("should verify will invoke correct methods", () => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + "delivery", + { paramKey: "prime" }, + { entityId: 25 }, + { search: "test-team" }, + RouteSegments.of( + "entity/25", + { deferredLoading: true }, + { entityId: 25 }, + { search: "test-team" }, + RouteSegments.of( + "domain/context", + { entityKey: "second" }, + {}, + { search: "test-team" }, + null, + ), + ), + ), + "domain/context/entity/25/delivery", + ); + const factoryCreateSpy = spyOn( + RouteStateFactory.prototype, + "create", + ).and.returnValue(routeState); - // When - const result = service.serialize({ - url: 'domain/context/entity/25/delivery', - root: rootRouteSnapshot, - toString: () => '' - }); + const routeSnapshotChild2 = createRouteSnapshot({ + url: [ + new UrlSegment("domain/context", {}), + new UrlSegment("entity/25", {}), + new UrlSegment("delivery", {}), + ], + data: { entityKey: "second" }, + params: { entityId: 25 }, + queryParams: { search: "team-test" }, + outlet: "router-outlet", + component: ComponentStub, + routeConfig: new RouteConfigStub( + "delivery", + ComponentStub, + "router-outlet", + ), + }); + const routeSnapshotChild1 = createRouteSnapshot({ + url: [ + new UrlSegment("domain/context", {}), + new UrlSegment("entity/25", {}), + ], + data: { deferredLoading: true }, + params: { entityId: 25 }, + queryParams: { search: "team-test" }, + outlet: "router-outlet", + component: ComponentStub, + routeConfig: new RouteConfigStub( + "entity/25", + ComponentStub, + "router-outlet", + ), + firstChild: routeSnapshotChild2, + }); + const rootRouteSnapshot = createRouteSnapshot({ + url: [new UrlSegment("domain/context", {})], + data: { paramKey: "prime" }, + params: {}, + queryParams: { search: "team-test" }, + outlet: "router-outlet", + component: ComponentStub, + routeConfig: new RouteConfigStub( + "domain/context", + ComponentStub, + "router-outlet", + ), + firstChild: routeSnapshotChild1, + }); - // Then - expect(factoryCreateSpy).toHaveBeenCalledWith(routeSnapshotChild2, 'domain/context/entity/25/delivery'); - expect(result).toBe(routeState); - }); + // When + const result = service.serialize({ + url: "domain/context/entity/25/delivery", + root: rootRouteSnapshot, + toString: () => "", }); + + // Then + expect(factoryCreateSpy).toHaveBeenCalledWith( + routeSnapshotChild2, + "domain/context/entity/25/delivery", + ); + expect(result).toBe(routeState); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts index 5cd0a59e03..ffade0bb45 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts @@ -3,38 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; -import { RouterStateSnapshot } from '@angular/router'; +import { Injectable } from "@angular/core"; +import { RouterStateSnapshot } from "@angular/router"; -import { RouterStateSerializer } from '@ngrx/router-store'; +import { RouterStateSerializer } from "@ngrx/router-store"; -import { RouteState } from '../../model'; -import { RouteStateFactory } from '../../factory'; +import { RouteState } from "../../model"; +import { RouteStateFactory } from "../../factory"; /** * ** Shared Router Serializer implements NgRx RouterStateSerializer. */ @Injectable() export class SharedRouterSerializer implements RouterStateSerializer { - private readonly _routeStateFactory: RouteStateFactory; - - /** - * ** Constructor. - */ - constructor() { - this._routeStateFactory = new RouteStateFactory(); + private readonly _routeStateFactory: RouteStateFactory; + + /** + * ** Constructor. + */ + constructor() { + this._routeStateFactory = new RouteStateFactory(); + } + + /** + * @inheritDoc + */ + serialize(routerState: RouterStateSnapshot): RouteState { + let routeSnapshot = routerState.root; + + while (routeSnapshot.firstChild) { + routeSnapshot = routeSnapshot.firstChild; } - /** - * @inheritDoc - */ - serialize(routerState: RouterStateSnapshot): RouteState { - let routeSnapshot = routerState.root; - - while (routeSnapshot.firstChild) { - routeSnapshot = routeSnapshot.firstChild; - } - - return this._routeStateFactory.create(routeSnapshot, routerState.url); - } + return this._routeStateFactory.create(routeSnapshot, routerState.url); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts index bc50e0cd73..385f83cbca 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './route.model'; +export * from "./route.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts index f8c4c481a4..3b2e7dc837 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts @@ -3,638 +3,779 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RouterState, RouteSegments, RouteState } from './route.model'; +import { RouterState, RouteSegments, RouteState } from "./route.model"; + +describe("Route Classes", () => { + describe("RouteSegments", () => { + it("should verify instance is created", () => { + // When + const instance = new RouteSegments( + "domain/context", + {}, + {}, + {}, + null, + null, + ); + + // Then + expect(instance).toBeDefined(); + }); -describe('Route Classes', () => { - describe('RouteSegments', () => { - it('should verify instance is created', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new RouteSegments('domain/context', {}, {}, {}, null, null); + const instance = RouteSegments.of( + "domain/context", + {}, + {}, + null, + null, + ); // Then - expect(instance).toBeDefined(); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RouteSegments.of('domain/context', {}, {}, null, null); - - // Then - expect(instance).toBeInstanceOf(RouteSegments); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with default values', () => { - // When - const instance = RouteSegments.empty(); - - // Then - expect(instance).toBeInstanceOf(RouteSegments); - expect(instance.params).toEqual({}); - expect(instance.queryParams).toEqual({}); - expect(instance.routePath).toEqual(''); - }); - }); - }); - }); - - describe('Methods::', () => { - describe('|getData|', () => { - it('should verify will return param', () => { - // Given - const routeSegments = RouteSegments.of( - 'bill/105/view', - { paramKey: 'prime' }, - { billId: '105' }, - {}, - RouteSegments.of( - 'entity/17/explore', - { deferredLoad: true }, - { entityId: '17' }, - null, - RouteSegments.of('domain/context', null, null, null, null, 'domain/context'), - 'entity/:entityId/explore' - ), - 'bill/:billId/view' - ); - - // When - const param1 = routeSegments.getData('randomParam'); - const param2 = routeSegments.getData('paramKey'); - const param3 = routeSegments.getData('deferredLoad'); - - // Then - expect(param1).toBeUndefined(); - expect(param2).toEqual('prime'); - expect(param3).toEqual(true); - }); - }); - - describe('|getParam|', () => { - it('should verify will return param', () => { - // Given - const routeSegments = RouteSegments.of( - 'bill/105', - {}, - { billId: '105' }, - {}, - RouteSegments.of('entity/17', {}, { entityId: '17' }, {}, null, 'entity/:entityId'), - 'bill/:billId' - ); - - // When - const param1 = routeSegments.getParam('randomParam'); - const param2 = routeSegments.getParam('billId'); - const param3 = routeSegments.getParam('entityId'); - - // Then - expect(param1).toBeUndefined(); - expect(param2).toEqual('105'); - expect(param3).toEqual('17'); - }); - }); - - describe('|getQueryParam|', () => { - it('should verify will return queryParam', () => { - // Given - const routeSegments = RouteSegments.of( - 'bill/105', - {}, - { billId: '105' }, - { search: 'test-team', activeUser: 'aUser' }, - RouteSegments.of( - 'entity/17', - {}, - { entityId: '17' }, - { search: 'test-team', activeUser: 'aUser' }, - RouteSegments.of( - 'domain/context', - {}, - {}, - { search: 'test-team', activeUser: 'aUser' }, - null, - 'domain/context' - ), - 'entity/:entityId' - ), - 'bill/:billId' - ); - - // When - const queryParam1 = routeSegments.getQueryParam('entityId'); - const queryParam2 = routeSegments.getQueryParam('search'); - const queryParam3 = routeSegments.getQueryParam('activeUser'); - - // Then - expect(queryParam1).toBeUndefined(); - expect(queryParam2).toEqual('test-team'); - expect(queryParam3).toEqual('aUser'); - }); - }); - }); - - describe('Getters/Setters::', () => { - describe('|GET -> routePathSegments|', () => { - it('should verify will return routePathSegments', () => { - // Given - const routeSegments = RouteSegments.of( - 'bill/105/view', - {}, - null, - null, - RouteSegments.of( - 'entity/17/explore', - {}, - null, - null, - RouteSegments.of('domain/context', null, null, null, null, 'domain/context'), - 'entity/:entityId/explore' - ), - 'bill/:billId/view' - ); - - // When - const routePathSegments = routeSegments.routePathSegments; - - // Then - expect(routePathSegments).toEqual(['domain/context', 'entity/17/explore', 'bill/105/view']); - }); - }); - - describe('|GET -> configPathSegments|', () => { - it('should verify will return configPathSegments', () => { - // Given - const routeSegments = RouteSegments.of( - 'bill/105/view', - {}, - null, - null, - RouteSegments.of( - 'entity/17/explore', - {}, - null, - null, - RouteSegments.of('domain/context', null, null, null, null, 'domain/context'), - 'entity/:entityId/explore' - ), - 'bill/:billId/view' - ); - - // When - const configPathSegments = routeSegments.configPathSegments; - - // Then - expect(configPathSegments).toEqual(['domain/context', 'entity/:entityId/explore', 'bill/:billId/view']); - }); - }); + expect(instance).toBeInstanceOf(RouteSegments); + }); }); - }); - describe('RouteState', () => { - let routeState: RouteState; - let routeSegments: RouteSegments; + describe("|empty|", () => { + it("should verify will create empty instance with default values", () => { + // When + const instance = RouteSegments.empty(); - beforeEach(() => { - routeSegments = RouteSegments.of( - 'bill/105/view', + // Then + expect(instance).toBeInstanceOf(RouteSegments); + expect(instance.params).toEqual({}); + expect(instance.queryParams).toEqual({}); + expect(instance.routePath).toEqual(""); + }); + }); + }); + }); + + describe("Methods::", () => { + describe("|getData|", () => { + it("should verify will return param", () => { + // Given + const routeSegments = RouteSegments.of( + "bill/105/view", + { paramKey: "prime" }, + { billId: "105" }, + {}, + RouteSegments.of( + "entity/17/explore", + { deferredLoad: true }, + { entityId: "17" }, + null, + RouteSegments.of( + "domain/context", + null, + null, + null, + null, + "domain/context", + ), + "entity/:entityId/explore", + ), + "bill/:billId/view", + ); + + // When + const param1 = routeSegments.getData("randomParam"); + const param2 = routeSegments.getData("paramKey"); + const param3 = routeSegments.getData("deferredLoad"); + + // Then + expect(param1).toBeUndefined(); + expect(param2).toEqual("prime"); + expect(param3).toEqual(true); + }); + }); + + describe("|getParam|", () => { + it("should verify will return param", () => { + // Given + const routeSegments = RouteSegments.of( + "bill/105", + {}, + { billId: "105" }, + {}, + RouteSegments.of( + "entity/17", + {}, + { entityId: "17" }, + {}, + null, + "entity/:entityId", + ), + "bill/:billId", + ); + + // When + const param1 = routeSegments.getParam("randomParam"); + const param2 = routeSegments.getParam("billId"); + const param3 = routeSegments.getParam("entityId"); + + // Then + expect(param1).toBeUndefined(); + expect(param2).toEqual("105"); + expect(param3).toEqual("17"); + }); + }); + + describe("|getQueryParam|", () => { + it("should verify will return queryParam", () => { + // Given + const routeSegments = RouteSegments.of( + "bill/105", + {}, + { billId: "105" }, + { search: "test-team", activeUser: "aUser" }, + RouteSegments.of( + "entity/17", + {}, + { entityId: "17" }, + { search: "test-team", activeUser: "aUser" }, + RouteSegments.of( + "domain/context", + {}, {}, + { search: "test-team", activeUser: "aUser" }, + null, + "domain/context", + ), + "entity/:entityId", + ), + "bill/:billId", + ); + + // When + const queryParam1 = routeSegments.getQueryParam("entityId"); + const queryParam2 = routeSegments.getQueryParam("search"); + const queryParam3 = routeSegments.getQueryParam("activeUser"); + + // Then + expect(queryParam1).toBeUndefined(); + expect(queryParam2).toEqual("test-team"); + expect(queryParam3).toEqual("aUser"); + }); + }); + }); + + describe("Getters/Setters::", () => { + describe("|GET -> routePathSegments|", () => { + it("should verify will return routePathSegments", () => { + // Given + const routeSegments = RouteSegments.of( + "bill/105/view", + {}, + null, + null, + RouteSegments.of( + "entity/17/explore", + {}, + null, + null, + RouteSegments.of( + "domain/context", + null, + null, + null, + null, + "domain/context", + ), + "entity/:entityId/explore", + ), + "bill/:billId/view", + ); + + // When + const routePathSegments = routeSegments.routePathSegments; + + // Then + expect(routePathSegments).toEqual([ + "domain/context", + "entity/17/explore", + "bill/105/view", + ]); + }); + }); + + describe("|GET -> configPathSegments|", () => { + it("should verify will return configPathSegments", () => { + // Given + const routeSegments = RouteSegments.of( + "bill/105/view", + {}, + null, + null, + RouteSegments.of( + "entity/17/explore", + {}, + null, + null, + RouteSegments.of( + "domain/context", + null, + null, null, - { 'test_search*': '^test-team$', 'test_@active-user': '%aUser%' }, - RouteSegments.of( - 'entity/17/explore', - {}, - null, - null, - RouteSegments.of('domain/context', {}, null, null, null, 'domain/context'), - 'entity/:entityId/explore' - ), - 'bill/:billId/view' + null, + "domain/context", + ), + "entity/:entityId/explore", + ), + "bill/:billId/view", + ); + + // When + const configPathSegments = routeSegments.configPathSegments; + + // Then + expect(configPathSegments).toEqual([ + "domain/context", + "entity/:entityId/explore", + "bill/:billId/view", + ]); + }); + }); + }); + }); + + describe("RouteState", () => { + let routeState: RouteState; + let routeSegments: RouteSegments; + + beforeEach(() => { + routeSegments = RouteSegments.of( + "bill/105/view", + {}, + null, + { "test_search*": "^test-team$", "test_@active-user": "%aUser%" }, + RouteSegments.of( + "entity/17/explore", + {}, + null, + null, + RouteSegments.of( + "domain/context", + {}, + null, + null, + null, + "domain/context", + ), + "entity/:entityId/explore", + ), + "bill/:billId/view", + ); + + routeState = RouteState.of( + routeSegments, + "domain/context/entity/17/explore/bill/105/view", + ); + }); + + it("should verify instance is created", () => { + // When + const instance = new RouteState( + new RouteSegments("domain/context", {}, {}, null, null), + "domain/context", + ); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = RouteState.of( + new RouteSegments("domain/context", {}, {}, null, null), + "domain/context", ); - routeState = RouteState.of(routeSegments, 'domain/context/entity/17/explore/bill/105/view'); + // Then + expect(instance).toBeInstanceOf(RouteState); + }); }); - it('should verify instance is created', () => { + describe("|empty|", () => { + it("should verify will create empty instance with default values", () => { // When - const instance = new RouteState(new RouteSegments('domain/context', {}, {}, null, null), 'domain/context'); + const instance = RouteState.empty(); // Then - expect(instance).toBeDefined(); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RouteState.of(new RouteSegments('domain/context', {}, {}, null, null), 'domain/context'); - - // Then - expect(instance).toBeInstanceOf(RouteState); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with default values', () => { - // When - const instance = RouteState.empty(); - - // Then - expect(instance).toBeInstanceOf(RouteState); - expect(instance.routeSegments).toEqual(RouteSegments.empty()); - expect(instance.url).toEqual(''); - }); - }); - }); - }); - - describe('Getters/Setters::', () => { - describe('|GET -> absoluteRoutePath|', () => { - it('should verify will return location', () => { - // When - const location = routeState.absoluteRoutePath; - - // Then - expect(location).toEqual('/domain/context/entity/17/explore/bill/105/view'); - }); - }); - - describe('|GET -> routePath|', () => { - it('should verify will return routePath', () => { - // When - const routePath = routeState.routePath; - - // Then - expect(routePath).toEqual('bill/105/view'); - }); - }); - - describe('|GET -> routePathSegments|', () => { - it('should verify will return routePathSegments', () => { - // When - const routePathSegments = routeState.routePathSegments; - - // Then - expect(routePathSegments).toEqual(['domain/context', 'entity/17/explore', 'bill/105/view']); - }); - }); - - describe('|GET -> configPath|', () => { - it('should verify will return configPath', () => { - // When - const configPath = routeState.configPath; - - // Then - expect(configPath).toEqual('bill/:billId/view'); - }); - }); - - describe('|GET -> absoluteConfigPath|', () => { - it('should verify will return absoluteConfigPath', () => { - // When - const absoluteConfigPath = routeState.absoluteConfigPath; - - // Then - expect(absoluteConfigPath).toEqual('/domain/context/entity/:entityId/explore/bill/:billId/view'); - }); - }); - - describe('|GET -> configPathSegments|', () => { - it('should verify will return configPathSegments', () => { - // When - const configPathSegments = routeState.configPathSegments; - - // Then - expect(configPathSegments).toEqual(['domain/context', 'entity/:entityId/explore', 'bill/:billId/view']); - }); - }); - - describe('|GET -> queryParams|', () => { - it('should verify will return queryParams', () => { - // When - const queryParams = routeState.queryParams; - - // Then - expect(queryParams).toBe(routeState.routeSegments.queryParams); - }); - }); - }); - - describe('Methods::', () => { - describe('|serializeQueryParams|', () => { - it('should verify will serialize queryParams in string', () => { - // When - const query = routeState.serializeQueryParams(); - - // Then - expect(query).toEqual('test_search*=%5Etest-team%24&test_%40active-user=%25aUser%25'); - }); - - it('should verify will return empty string when no queryParams', () => { - // Given - routeState = RouteState.of( - RouteSegments.of( - routeState.routeSegments.routePath, - routeState.routeSegments.data, - routeState.routeSegments.params, - {}, - routeState.routeSegments.parent, - routeState.routeSegments.configPath - ), - routeState.url - ); - - // When - const query = routeState.serializeQueryParams(); - - // Then - expect(query).toEqual(''); - }); - }); - - describe('|getUrl|', () => { - it('should verify will return correct value', () => { - // When - const url = routeState.getUrl(); - - // Then - expect(url).toEqual( - '/domain/context/entity/17/explore/bill/105/view?test_search*=%5Etest-team%24&test_%40active-user=%25aUser%25' - ); - }); - }); - - describe('|getData|', () => { - it('should verify will invoke correct methods', () => { - // Given - const getDataSpy = spyOn(RouteSegments.prototype, 'getData').and.returnValue('loading'); - - // When - const data = routeState.getData('primeKey'); - - // Then - expect(getDataSpy).toHaveBeenCalledWith('primeKey'); - expect(data).toEqual('loading'); - }); - }); - - describe('|getParam|', () => { - it('should verify will invoke correct methods', () => { - // Given - const getParamSpy = spyOn(RouteSegments.prototype, 'getParam').and.returnValue('test-value'); - - // When - const param = routeState.getParam('random-key'); - - // Then - expect(getParamSpy).toHaveBeenCalledWith('random-key'); - expect(param).toEqual('test-value'); - }); - }); - - describe('|getQueryParam|', () => { - it('should verify will invoke correct methods', () => { - // Given - const getQueryParamSpy = spyOn(RouteSegments.prototype, 'getQueryParam').and.returnValue('test-value'); - - // When - const queryParam = routeState.getQueryParam('random-key'); - - // Then - expect(getQueryParamSpy).toHaveBeenCalledWith('random-key'); - expect(queryParam).toEqual('test-value'); - }); - }); - - describe('|getParentAbsoluteRoutePath|', () => { - it('should verify will return correct value', () => { - // When - const parentAbsoluteRoutePath = routeState.getParentAbsoluteRoutePath(); - - // Then - expect(parentAbsoluteRoutePath).toEqual('/domain/context/entity/17/explore'); - }); - }); - - describe('|toJSON|', () => { - it('should verify will return correct object for serialization', () => { - // When - const objForSerialize = routeState.toJSON(); - - // Then - expect(objForSerialize).toEqual({ - routeSegments, - url: 'domain/context/entity/17/explore/bill/105/view', - routePath: 'bill/105/view', - absoluteRoutePath: '/domain/context/entity/17/explore/bill/105/view', - routePathSegments: ['domain/context', 'entity/17/explore', 'bill/105/view'], - configPath: 'bill/:billId/view', - absoluteConfigPath: '/domain/context/entity/:entityId/explore/bill/:billId/view', - configPathSegments: ['domain/context', 'entity/:entityId/explore', 'bill/:billId/view'], - queryParams: { 'test_search*': '^test-team$', 'test_@active-user': '%aUser%' } - }); - }); - }); + expect(instance).toBeInstanceOf(RouteState); + expect(instance.routeSegments).toEqual(RouteSegments.empty()); + expect(instance.url).toEqual(""); + }); }); + }); }); - describe('RouterState', () => { - let routeSegments: RouteSegments; - let routeState: RouteState; - let routerState: RouterState; - let previousRouterState: RouterState; + describe("Getters/Setters::", () => { + describe("|GET -> absoluteRoutePath|", () => { + it("should verify will return location", () => { + // When + const location = routeState.absoluteRoutePath; - beforeEach(() => { - routeSegments = RouteSegments.of( - 'bill/105/view', - {}, - null, - { 'test_search*': '^test-team$', 'test_@active-user': '%aUser%' }, - RouteSegments.of( - 'entity/17/explore', - {}, - null, - null, - RouteSegments.of('domain/context', {}, null, null, null, 'domain/context'), - 'entity/:entityId/explore' - ), - 'bill/:billId/view' - ); + // Then + expect(location).toEqual( + "/domain/context/entity/17/explore/bill/105/view", + ); + }); + }); - routeState = RouteState.of(routeSegments, 'domain/context/entity/17/explore/bill/105/view'); + describe("|GET -> routePath|", () => { + it("should verify will return routePath", () => { + // When + const routePath = routeState.routePath; - routerState = RouterState.of(routeState, 15); + // Then + expect(routePath).toEqual("bill/105/view"); + }); + }); + + describe("|GET -> routePathSegments|", () => { + it("should verify will return routePathSegments", () => { + // When + const routePathSegments = routeState.routePathSegments; + + // Then + expect(routePathSegments).toEqual([ + "domain/context", + "entity/17/explore", + "bill/105/view", + ]); + }); + }); + + describe("|GET -> configPath|", () => { + it("should verify will return configPath", () => { + // When + const configPath = routeState.configPath; + + // Then + expect(configPath).toEqual("bill/:billId/view"); + }); + }); - previousRouterState = RouterState.of(RouteState.empty(), 14); - previousRouterState.previousStates.push( - RouterState.of(RouteState.empty(), 13), - RouterState.of(RouteState.empty(), 12), - RouterState.of(RouteState.empty(), 11) + describe("|GET -> absoluteConfigPath|", () => { + it("should verify will return absoluteConfigPath", () => { + // When + const absoluteConfigPath = routeState.absoluteConfigPath; + + // Then + expect(absoluteConfigPath).toEqual( + "/domain/context/entity/:entityId/explore/bill/:billId/view", + ); + }); + }); + + describe("|GET -> configPathSegments|", () => { + it("should verify will return configPathSegments", () => { + // When + const configPathSegments = routeState.configPathSegments; + + // Then + expect(configPathSegments).toEqual([ + "domain/context", + "entity/:entityId/explore", + "bill/:billId/view", + ]); + }); + }); + + describe("|GET -> queryParams|", () => { + it("should verify will return queryParams", () => { + // When + const queryParams = routeState.queryParams; + + // Then + expect(queryParams).toBe(routeState.routeSegments.queryParams); + }); + }); + }); + + describe("Methods::", () => { + describe("|serializeQueryParams|", () => { + it("should verify will serialize queryParams in string", () => { + // When + const query = routeState.serializeQueryParams(); + + // Then + expect(query).toEqual( + "test_search*=%5Etest-team%24&test_%40active-user=%25aUser%25", + ); + }); + + it("should verify will return empty string when no queryParams", () => { + // Given + routeState = RouteState.of( + RouteSegments.of( + routeState.routeSegments.routePath, + routeState.routeSegments.data, + routeState.routeSegments.params, + {}, + routeState.routeSegments.parent, + routeState.routeSegments.configPath, + ), + routeState.url, + ); + + // When + const query = routeState.serializeQueryParams(); + + // Then + expect(query).toEqual(""); + }); + }); + + describe("|getUrl|", () => { + it("should verify will return correct value", () => { + // When + const url = routeState.getUrl(); + + // Then + expect(url).toEqual( + "/domain/context/entity/17/explore/bill/105/view?test_search*=%5Etest-team%24&test_%40active-user=%25aUser%25", + ); + }); + }); + + describe("|getData|", () => { + it("should verify will invoke correct methods", () => { + // Given + const getDataSpy = spyOn( + RouteSegments.prototype, + "getData", + ).and.returnValue("loading"); + + // When + const data = routeState.getData("primeKey"); + + // Then + expect(getDataSpy).toHaveBeenCalledWith("primeKey"); + expect(data).toEqual("loading"); + }); + }); + + describe("|getParam|", () => { + it("should verify will invoke correct methods", () => { + // Given + const getParamSpy = spyOn( + RouteSegments.prototype, + "getParam", + ).and.returnValue("test-value"); + + // When + const param = routeState.getParam("random-key"); + + // Then + expect(getParamSpy).toHaveBeenCalledWith("random-key"); + expect(param).toEqual("test-value"); + }); + }); + + describe("|getQueryParam|", () => { + it("should verify will invoke correct methods", () => { + // Given + const getQueryParamSpy = spyOn( + RouteSegments.prototype, + "getQueryParam", + ).and.returnValue("test-value"); + + // When + const queryParam = routeState.getQueryParam("random-key"); + + // Then + expect(getQueryParamSpy).toHaveBeenCalledWith("random-key"); + expect(queryParam).toEqual("test-value"); + }); + }); + + describe("|getParentAbsoluteRoutePath|", () => { + it("should verify will return correct value", () => { + // When + const parentAbsoluteRoutePath = + routeState.getParentAbsoluteRoutePath(); + + // Then + expect(parentAbsoluteRoutePath).toEqual( + "/domain/context/entity/17/explore", + ); + }); + }); + + describe("|toJSON|", () => { + it("should verify will return correct object for serialization", () => { + // When + const objForSerialize = routeState.toJSON(); + + // Then + expect(objForSerialize).toEqual({ + routeSegments, + url: "domain/context/entity/17/explore/bill/105/view", + routePath: "bill/105/view", + absoluteRoutePath: + "/domain/context/entity/17/explore/bill/105/view", + routePathSegments: [ + "domain/context", + "entity/17/explore", + "bill/105/view", + ], + configPath: "bill/:billId/view", + absoluteConfigPath: + "/domain/context/entity/:entityId/explore/bill/:billId/view", + configPathSegments: [ + "domain/context", + "entity/:entityId/explore", + "bill/:billId/view", + ], + queryParams: { + "test_search*": "^test-team$", + "test_@active-user": "%aUser%", + }, + }); + }); + }); + }); + }); + + describe("RouterState", () => { + let routeSegments: RouteSegments; + let routeState: RouteState; + let routerState: RouterState; + let previousRouterState: RouterState; + + beforeEach(() => { + routeSegments = RouteSegments.of( + "bill/105/view", + {}, + null, + { "test_search*": "^test-team$", "test_@active-user": "%aUser%" }, + RouteSegments.of( + "entity/17/explore", + {}, + null, + null, + RouteSegments.of( + "domain/context", + {}, + null, + null, + null, + "domain/context", + ), + "entity/:entityId/explore", + ), + "bill/:billId/view", + ); + + routeState = RouteState.of( + routeSegments, + "domain/context/entity/17/explore/bill/105/view", + ); + + routerState = RouterState.of(routeState, 15); + + previousRouterState = RouterState.of(RouteState.empty(), 14); + previousRouterState.previousStates.push( + RouterState.of(RouteState.empty(), 13), + RouterState.of(RouteState.empty(), 12), + RouterState.of(RouteState.empty(), 11), + ); + }); + + it("should verify instance is created", () => { + // When + const instance = new RouterState( + new RouteState( + new RouteSegments("domain/context", {}, {}, null, null), + "domain/context", + ), + 8, + ); + + // Then + expect(instance).toBeDefined(); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const instance = RouterState.of( + new RouteState( + new RouteSegments("domain/context", {}, {}, null, null), + "domain/context", + ), + 11, ); + + // Then + expect(instance).toBeInstanceOf(RouterState); + }); }); - it('should verify instance is created', () => { + describe("|empty|", () => { + it("should verify will create empty instance with default values", () => { // When - const instance = new RouterState(new RouteState(new RouteSegments('domain/context', {}, {}, null, null), 'domain/context'), 8); + const instance = RouterState.empty(); // Then - expect(instance).toBeDefined(); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RouterState.of( - new RouteState(new RouteSegments('domain/context', {}, {}, null, null), 'domain/context'), - 11 - ); - - // Then - expect(instance).toBeInstanceOf(RouterState); - }); - }); - - describe('|empty|', () => { - it('should verify will create empty instance with default values', () => { - // When - const instance = RouterState.empty(); - - // Then - expect(instance).toBeInstanceOf(RouterState); - expect(instance.state).toEqual(RouteState.empty()); - expect(instance.navigationId).toEqual(null); - expect(instance.previousStates).toEqual([]); - }); - }); - }); - }); - - describe('Methods::', () => { - describe('|getPrevious|', () => { - beforeEach(() => { - routerState.previousStates.push(...previousRouterState.previousStates); - previousRouterState.previousStates.length = 0; - routerState.previousStates.unshift(previousRouterState); - }); - - it('should verify will return the first before current as default', () => { - // When - const previous = routerState.getPrevious(); - - // Then - expect(previous).toBe(previousRouterState); - }); - - it('should verify will return the first before current for index 0', () => { - // When - const previous = routerState.getPrevious(0); - - // Then - expect(previous).toBe(previousRouterState); - }); - - it('should verify will return the third before current for index 2', () => { - // When - const previous = routerState.getPrevious(2); - - // Then - expect(previous).toBe(routerState.previousStates[2]); - }); - - it('should verify will return null for number less than 0', () => { - // When - const previous = routerState.getPrevious(-1); - - // Then - expect(previous).toBe(null); - }); - - it('should verify will return the first before current for index not a number', () => { - // When - const previous = routerState.getPrevious(null); - - // Then - expect(previous).toBe(previousRouterState); - }); - - it('should verify will return null for index that is out of bound of stored States', () => { - // When - const previous = routerState.getPrevious(4); - - // Then - expect(previous).toBe(null); - }); - }); - - describe('|appendPrevious|', () => { - it('should verify will add previousStates to current one', () => { - // Given - const firstElement = RouterState.of(RouteState.empty(), 14); - const forthElement = RouterState.of(RouteState.empty(), 11); - - // When - routerState.appendPrevious(previousRouterState); - - // Then - expect(routerState.previousStates.length).toEqual(4); - expect(routerState.previousStates[0]).toEqual(firstElement); - expect(routerState.previousStates[3]).toEqual(forthElement); - }); - - it('should verify will pop the oldest one and will unshift the new one when buffer has 10 elements', () => { - // Given - previousRouterState.previousStates.push( - RouterState.of(RouteState.empty(), 10), - RouterState.of(RouteState.empty(), 9), - RouterState.of(RouteState.empty(), 8), - RouterState.of(RouteState.empty(), 7), - RouterState.of(RouteState.empty(), 6), - RouterState.of(RouteState.empty(), 5), - RouterState.of(RouteState.empty(), 4) - ); - - // Then 1 - expect(routerState.previousStates.length).toEqual(0); - expect(routerState.previousStates[0]).toBeUndefined(); - expect(previousRouterState.navigationId).toEqual(14); - expect(previousRouterState.previousStates[0]).toEqual(RouterState.of(RouteState.empty(), 13)); - expect(previousRouterState.previousStates[9]).toEqual(RouterState.of(RouteState.empty(), 4)); - - // When - routerState.appendPrevious(previousRouterState); - - // Then 2 - expect(routerState.previousStates.length).toEqual(10); - expect(routerState.previousStates[0]).toEqual(RouterState.of(RouteState.empty(), 14)); - expect(routerState.previousStates[9]).toEqual(RouterState.of(RouteState.empty(), 5)); - }); - - it(`should verify won't add new state when previousState has same navigationId like current`, () => { - // Given - routerState = RouterState.of(RouteState.empty(), 14); - - // Then 1 - expect(routerState.previousStates.length).toEqual(0); - expect(previousRouterState.previousStates.length).toEqual(3); - expect(previousRouterState.previousStates[0]).toEqual(RouterState.of(RouteState.empty(), 13)); - expect(previousRouterState.previousStates[2]).toEqual(RouterState.of(RouteState.empty(), 11)); - - // When - routerState.appendPrevious(previousRouterState); - - // Then 2 - expect(routerState.previousStates.length).toEqual(3); - expect(routerState.navigationId).toEqual(14); - expect(routerState.previousStates[0]).toEqual(RouterState.of(RouteState.empty(), 13)); - expect(routerState.previousStates[2]).toEqual(RouterState.of(RouteState.empty(), 11)); - }); - }); + expect(instance).toBeInstanceOf(RouterState); + expect(instance.state).toEqual(RouteState.empty()); + expect(instance.navigationId).toEqual(null); + expect(instance.previousStates).toEqual([]); + }); + }); + }); + }); + + describe("Methods::", () => { + describe("|getPrevious|", () => { + beforeEach(() => { + routerState.previousStates.push( + ...previousRouterState.previousStates, + ); + previousRouterState.previousStates.length = 0; + routerState.previousStates.unshift(previousRouterState); + }); + + it("should verify will return the first before current as default", () => { + // When + const previous = routerState.getPrevious(); + + // Then + expect(previous).toBe(previousRouterState); + }); + + it("should verify will return the first before current for index 0", () => { + // When + const previous = routerState.getPrevious(0); + + // Then + expect(previous).toBe(previousRouterState); + }); + + it("should verify will return the third before current for index 2", () => { + // When + const previous = routerState.getPrevious(2); + + // Then + expect(previous).toBe(routerState.previousStates[2]); + }); + + it("should verify will return null for number less than 0", () => { + // When + const previous = routerState.getPrevious(-1); + + // Then + expect(previous).toBe(null); + }); + + it("should verify will return the first before current for index not a number", () => { + // When + const previous = routerState.getPrevious(null); + + // Then + expect(previous).toBe(previousRouterState); + }); + + it("should verify will return null for index that is out of bound of stored States", () => { + // When + const previous = routerState.getPrevious(4); + + // Then + expect(previous).toBe(null); + }); + }); + + describe("|appendPrevious|", () => { + it("should verify will add previousStates to current one", () => { + // Given + const firstElement = RouterState.of(RouteState.empty(), 14); + const forthElement = RouterState.of(RouteState.empty(), 11); + + // When + routerState.appendPrevious(previousRouterState); + + // Then + expect(routerState.previousStates.length).toEqual(4); + expect(routerState.previousStates[0]).toEqual(firstElement); + expect(routerState.previousStates[3]).toEqual(forthElement); + }); + + it("should verify will pop the oldest one and will unshift the new one when buffer has 10 elements", () => { + // Given + previousRouterState.previousStates.push( + RouterState.of(RouteState.empty(), 10), + RouterState.of(RouteState.empty(), 9), + RouterState.of(RouteState.empty(), 8), + RouterState.of(RouteState.empty(), 7), + RouterState.of(RouteState.empty(), 6), + RouterState.of(RouteState.empty(), 5), + RouterState.of(RouteState.empty(), 4), + ); + + // Then 1 + expect(routerState.previousStates.length).toEqual(0); + expect(routerState.previousStates[0]).toBeUndefined(); + expect(previousRouterState.navigationId).toEqual(14); + expect(previousRouterState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 13), + ); + expect(previousRouterState.previousStates[9]).toEqual( + RouterState.of(RouteState.empty(), 4), + ); + + // When + routerState.appendPrevious(previousRouterState); + + // Then 2 + expect(routerState.previousStates.length).toEqual(10); + expect(routerState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 14), + ); + expect(routerState.previousStates[9]).toEqual( + RouterState.of(RouteState.empty(), 5), + ); + }); + + it(`should verify won't add new state when previousState has same navigationId like current`, () => { + // Given + routerState = RouterState.of(RouteState.empty(), 14); + + // Then 1 + expect(routerState.previousStates.length).toEqual(0); + expect(previousRouterState.previousStates.length).toEqual(3); + expect(previousRouterState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 13), + ); + expect(previousRouterState.previousStates[2]).toEqual( + RouterState.of(RouteState.empty(), 11), + ); + + // When + routerState.appendPrevious(previousRouterState); + + // Then 2 + expect(routerState.previousStates.length).toEqual(3); + expect(routerState.navigationId).toEqual(14); + expect(routerState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 13), + ); + expect(routerState.previousStates[2]).toEqual( + RouterState.of(RouteState.empty(), 11), + ); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts index 98874e3ec3..3d09e60db5 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts @@ -5,415 +5,436 @@ /* eslint-disable @typescript-eslint/member-ordering */ -import { Params } from '@angular/router'; +import { Params } from "@angular/router"; -import { BaseRouterStoreState, RouterReducerState } from '@ngrx/router-store'; +import { BaseRouterStoreState, RouterReducerState } from "@ngrx/router-store"; -import { CollectionsUtil, PrimitivesNil } from '../../../utils'; +import { CollectionsUtil, PrimitivesNil } from "../../../utils"; -import { Serializable, TaurusRouteData } from '../../../common'; +import { Serializable, TaurusRouteData } from "../../../common"; /** * ** Route Segments Class. */ export class RouteSegments { - public readonly routePath: string; - public readonly data: TaurusRouteData; - public readonly params: Params; - public readonly queryParams: Params; - public readonly parent?: RouteSegments; - public readonly configPath?: string; - - /** - * ** Constructor. - */ - constructor( - routePath: string, - data: TaurusRouteData, - params: Params, - queryParams: Params, - parent?: RouteSegments, - configPath?: string - ) { - this.routePath = routePath ?? ''; - this.data = data || {}; - this.params = params || {}; - this.queryParams = queryParams || {}; - this.parent = parent; - this.configPath = configPath; - } - - /** - * ** Factory method. - */ - static of( - routePath?: string, - data?: TaurusRouteData, - params?: Params, - queryParams?: Params, - parent?: RouteSegments, - configPath?: string - ): RouteSegments { - return new RouteSegments(routePath, data, params, queryParams, parent, configPath); - } - - /** - * ** Factory method for empty RouteSegments. - */ - static empty(): RouteSegments { - return RouteSegments.of(null, null, null, null, null, null); - } - - /** - * ** Get RoutePath Segments. - */ - get routePathSegments(): string[] { - if (this.parent) { - return ([] as string[]).concat(this.parent.routePathSegments, this.routePath).filter((path) => path); - } - - return [this.routePath]; - } - - /** - * ** Get ConfigPath Segments. - */ - get configPathSegments(): string[] { - if (this.parent) { - return ([] as string[]).concat(this.parent.configPathSegments, this.configPath).filter((path) => path); - } - - return [this.configPath]; - } - - /** - * ** Get Data from Route configuration by key. - * - * - Return first (closest) found key starting from the current one. - */ - getData(key: string): T { - if (this.data[key]) { - return this.data[key] as T; - } - - if (this.parent) { - return this.parent.getData(key); - } - - return undefined; - } - - /** - * ** Get url param by key. - * - * - Return first (closest) found key starting from the current one. - */ - getParam(key: string): string { - if (this.params[key]) { - return this.params[key] as string; - } - - if (this.parent) { - return this.parent.getParam(key); - } - - return undefined; - } - - /** - * ** Get query param by key. - */ - getQueryParam(key: string): string { - if (this.queryParams[key]) { - return this.queryParams[key] as string; - } - - if (this.parent) { - return this.parent.getQueryParam(key); - } - - return undefined; - } + public readonly routePath: string; + public readonly data: TaurusRouteData; + public readonly params: Params; + public readonly queryParams: Params; + public readonly parent?: RouteSegments; + public readonly configPath?: string; + + /** + * ** Constructor. + */ + constructor( + routePath: string, + data: TaurusRouteData, + params: Params, + queryParams: Params, + parent?: RouteSegments, + configPath?: string, + ) { + this.routePath = routePath ?? ""; + this.data = data || {}; + this.params = params || {}; + this.queryParams = queryParams || {}; + this.parent = parent; + this.configPath = configPath; + } + + /** + * ** Factory method. + */ + static of( + routePath?: string, + data?: TaurusRouteData, + params?: Params, + queryParams?: Params, + parent?: RouteSegments, + configPath?: string, + ): RouteSegments { + return new RouteSegments( + routePath, + data, + params, + queryParams, + parent, + configPath, + ); + } + + /** + * ** Factory method for empty RouteSegments. + */ + static empty(): RouteSegments { + return RouteSegments.of(null, null, null, null, null, null); + } + + /** + * ** Get RoutePath Segments. + */ + get routePathSegments(): string[] { + if (this.parent) { + return ([] as string[]) + .concat(this.parent.routePathSegments, this.routePath) + .filter((path) => path); + } + + return [this.routePath]; + } + + /** + * ** Get ConfigPath Segments. + */ + get configPathSegments(): string[] { + if (this.parent) { + return ([] as string[]) + .concat(this.parent.configPathSegments, this.configPath) + .filter((path) => path); + } + + return [this.configPath]; + } + + /** + * ** Get Data from Route configuration by key. + * + * - Return first (closest) found key starting from the current one. + */ + getData(key: string): T { + if (this.data[key]) { + return this.data[key] as T; + } + + if (this.parent) { + return this.parent.getData(key); + } + + return undefined; + } + + /** + * ** Get url param by key. + * + * - Return first (closest) found key starting from the current one. + */ + getParam(key: string): string { + if (this.params[key]) { + return this.params[key] as string; + } + + if (this.parent) { + return this.parent.getParam(key); + } + + return undefined; + } + + /** + * ** Get query param by key. + */ + getQueryParam(key: string): string { + if (this.queryParams[key]) { + return this.queryParams[key] as string; + } + + if (this.parent) { + return this.parent.getQueryParam(key); + } + + return undefined; + } } /** * ** Route State Class. */ -export class RouteState implements BaseRouterStoreState, Serializable { - public readonly routeSegments: RouteSegments; - public readonly url: string; - - /** - * ** Constructor. - */ - constructor(routeSegments: RouteSegments, url: string) { - this.routeSegments = routeSegments ?? RouteSegments.empty(); - this.url = url ?? ''; - } - - /** - * ** Factory method. - */ - static of(routeSegments: RouteSegments, url: string): RouteState { - return new RouteState(routeSegments, url); - } - - /** - * ** Factory method for empty State. - */ - static empty(): RouteState { - return RouteState.of(null, null); - } - - /** - * ** Get serialized queryString. - */ - static serializeQueryParams(queryParams: unknown): string { - const paramsKeys = Object.keys(queryParams); - - if (!paramsKeys.length) { - return ''; - } - - return paramsKeys.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key] as string)}`).join('&'); - } - - /** - * ** Returns current RoutePath. - */ - get routePath(): string { - return this.routeSegments.routePath; - } - - /** - * ** Returns current Absolute RoutePath. - */ - get absoluteRoutePath(): string { - return RouteState._resolveAbsolutePath(this.routePathSegments); - } - - /** - * ** Returns the route paths for each route segment starting from the root. - */ - get routePathSegments(): string[] { - return this.routeSegments.routePathSegments; - } - - /** - * ** Returns current ConfigPath. - */ - get configPath(): string { - return this.routeSegments.configPath; - } - - /** - * ** Returns current Absolute ConfigPath. - */ - get absoluteConfigPath(): string { - return RouteState._resolveAbsolutePath(this.configPathSegments); - } - - /** - * ** Returns the config paths for each route segment starting from the root. - */ - get configPathSegments(): string[] { - return this.routeSegments.configPathSegments; - } - - /** - * ** Get all query params. - */ - get queryParams(): Params { - return this.routeSegments.queryParams; - } - - /** - * ** Get serialized queryString. - */ - serializeQueryParams(): string { - return RouteState.serializeQueryParams(this.queryParams); - } - - /** - * ** Get url including QueryParams. - */ - getUrl(): string { - return `${this.absoluteRoutePath}?${this.serializeQueryParams()}`; - } - - /** - * ** Get Data from Route configuration by key. - * - * - Return first (closest) found key starting from first RouteSegment. - */ - getData(key: string): T { - return this.routeSegments.getData(key); - } - - /** - * ** Get url param by key. - * - * - Return first (closest) found key starting from first RouteSegment. - */ - getParam(key: string): string { - return this.routeSegments.getParam(key); - } - - /** - * ** Get query param by key. - */ - getQueryParam(key: string): string { - return this.routeSegments.getQueryParam(key); - } - - /** - * ** Get Absolute ConfigPath. - */ - getAbsoluteConfigPath(): string { - return this.absoluteConfigPath; - } - - /** - * ** Get parent of current Absolute ConfigPath. - */ - getParentAbsoluteConfigPath(): string { - const configPathSegments = this.configPathSegments; - configPathSegments.pop(); - - return RouteState._resolveAbsolutePath(configPathSegments); - } - - /** - * ** Get Absolute RoutePath. - */ - getAbsoluteRoutePath(): string { - return RouteState._resolveAbsolutePath(this.routePathSegments); - } - - /** - * ** Get parent of current Absolute RoutePath. - */ - getParentAbsoluteRoutePath(): string { - const routePathSegments = this.routePathSegments; - routePathSegments.pop(); - - return RouteState._resolveAbsolutePath(routePathSegments); - } - - /** - * @inheritDoc - */ - toJSON(): SerializedRouteState { - return { - url: this.url, - routePath: this.routePath, - absoluteRoutePath: this.absoluteRoutePath, - routePathSegments: this.routePathSegments, - configPath: this.configPath, - absoluteConfigPath: this.absoluteConfigPath, - configPathSegments: this.configPathSegments, - queryParams: this.queryParams, - routeSegments: this.routeSegments - }; - } - - /** - * ** Resolve Absolute RoutePath from given routePathSegments. - */ - private static _resolveAbsolutePath(routePathSegments: string[]): string { - const path = routePathSegments.join('/').replace(/^\/+/, ''); - - if (path === '') { - return '/'; - } - - return `/${path}`; - } +export class RouteState + implements BaseRouterStoreState, Serializable +{ + public readonly routeSegments: RouteSegments; + public readonly url: string; + + /** + * ** Constructor. + */ + constructor(routeSegments: RouteSegments, url: string) { + this.routeSegments = routeSegments ?? RouteSegments.empty(); + this.url = url ?? ""; + } + + /** + * ** Factory method. + */ + static of(routeSegments: RouteSegments, url: string): RouteState { + return new RouteState(routeSegments, url); + } + + /** + * ** Factory method for empty State. + */ + static empty(): RouteState { + return RouteState.of(null, null); + } + + /** + * ** Get serialized queryString. + */ + static serializeQueryParams(queryParams: unknown): string { + const paramsKeys = Object.keys(queryParams); + + if (!paramsKeys.length) { + return ""; + } + + return paramsKeys + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key] as string)}`, + ) + .join("&"); + } + + /** + * ** Returns current RoutePath. + */ + get routePath(): string { + return this.routeSegments.routePath; + } + + /** + * ** Returns current Absolute RoutePath. + */ + get absoluteRoutePath(): string { + return RouteState._resolveAbsolutePath(this.routePathSegments); + } + + /** + * ** Returns the route paths for each route segment starting from the root. + */ + get routePathSegments(): string[] { + return this.routeSegments.routePathSegments; + } + + /** + * ** Returns current ConfigPath. + */ + get configPath(): string { + return this.routeSegments.configPath; + } + + /** + * ** Returns current Absolute ConfigPath. + */ + get absoluteConfigPath(): string { + return RouteState._resolveAbsolutePath(this.configPathSegments); + } + + /** + * ** Returns the config paths for each route segment starting from the root. + */ + get configPathSegments(): string[] { + return this.routeSegments.configPathSegments; + } + + /** + * ** Get all query params. + */ + get queryParams(): Params { + return this.routeSegments.queryParams; + } + + /** + * ** Get serialized queryString. + */ + serializeQueryParams(): string { + return RouteState.serializeQueryParams(this.queryParams); + } + + /** + * ** Get url including QueryParams. + */ + getUrl(): string { + return `${this.absoluteRoutePath}?${this.serializeQueryParams()}`; + } + + /** + * ** Get Data from Route configuration by key. + * + * - Return first (closest) found key starting from first RouteSegment. + */ + getData(key: string): T { + return this.routeSegments.getData(key); + } + + /** + * ** Get url param by key. + * + * - Return first (closest) found key starting from first RouteSegment. + */ + getParam(key: string): string { + return this.routeSegments.getParam(key); + } + + /** + * ** Get query param by key. + */ + getQueryParam(key: string): string { + return this.routeSegments.getQueryParam(key); + } + + /** + * ** Get Absolute ConfigPath. + */ + getAbsoluteConfigPath(): string { + return this.absoluteConfigPath; + } + + /** + * ** Get parent of current Absolute ConfigPath. + */ + getParentAbsoluteConfigPath(): string { + const configPathSegments = this.configPathSegments; + configPathSegments.pop(); + + return RouteState._resolveAbsolutePath(configPathSegments); + } + + /** + * ** Get Absolute RoutePath. + */ + getAbsoluteRoutePath(): string { + return RouteState._resolveAbsolutePath(this.routePathSegments); + } + + /** + * ** Get parent of current Absolute RoutePath. + */ + getParentAbsoluteRoutePath(): string { + const routePathSegments = this.routePathSegments; + routePathSegments.pop(); + + return RouteState._resolveAbsolutePath(routePathSegments); + } + + /** + * @inheritDoc + */ + toJSON(): SerializedRouteState { + return { + url: this.url, + routePath: this.routePath, + absoluteRoutePath: this.absoluteRoutePath, + routePathSegments: this.routePathSegments, + configPath: this.configPath, + absoluteConfigPath: this.absoluteConfigPath, + configPathSegments: this.configPathSegments, + queryParams: this.queryParams, + routeSegments: this.routeSegments, + }; + } + + /** + * ** Resolve Absolute RoutePath from given routePathSegments. + */ + private static _resolveAbsolutePath(routePathSegments: string[]): string { + const path = routePathSegments.join("/").replace(/^\/+/, ""); + + if (path === "") { + return "/"; + } + + return `/${path}`; + } } /** * ** Router state. */ export class RouterState implements RouterReducerState { - readonly state: RouteState; - readonly navigationId: number; - readonly previousStates: RouterState[]; - - /** - * ** Constructor. - */ - constructor(state: RouteState, navigationId: number) { - this.state = state ?? RouteState.empty(); - this.navigationId = navigationId ?? null; - this.previousStates = []; - } - - /** - * ** Factory method. - */ - static of(state: RouteState, navigationId: number): RouterState { - return new RouterState(state, navigationId); - } - - /** - * ** Factory method for empty State. - */ - static empty(): RouterState { - return RouterState.of(null, null); - } - - /** - * ** Returns previous RouterState if exist otherwise null. - * - * - Optional parameter could be provided to instruct which previous RouterState to return, default one is 0. - * - 0 means the first before current. - * - 1 means the second before current. - * - 2 means the third before current. - * - 3 ... etc... - */ - getPrevious(index = 0): RouterState | null { - const lookupIndex = CollectionsUtil.isNumber(index) ? index : 0; - - if (lookupIndex >= 0 && lookupIndex < this.previousStates.length) { - return this.previousStates[lookupIndex]; - } - - return null; - } - - /** - * ** Append previous RouterState[] to current One. - * - * - Internal API used in reducer, not for public use. - */ - appendPrevious(routerState: RouterState): void { - const previousStoredStates: RouterState[] = [...routerState.previousStates]; - const cleanedPreviousState = RouterState.of(routerState.state, routerState.navigationId); - - if (this.navigationId !== cleanedPreviousState.navigationId) { - if (previousStoredStates.length >= 10) { - previousStoredStates.pop(); - } - - previousStoredStates.unshift(cleanedPreviousState); - } - - this.previousStates.length = 0; - this.previousStates.push(...previousStoredStates); - } + readonly state: RouteState; + readonly navigationId: number; + readonly previousStates: RouterState[]; + + /** + * ** Constructor. + */ + constructor(state: RouteState, navigationId: number) { + this.state = state ?? RouteState.empty(); + this.navigationId = navigationId ?? null; + this.previousStates = []; + } + + /** + * ** Factory method. + */ + static of(state: RouteState, navigationId: number): RouterState { + return new RouterState(state, navigationId); + } + + /** + * ** Factory method for empty State. + */ + static empty(): RouterState { + return RouterState.of(null, null); + } + + /** + * ** Returns previous RouterState if exist otherwise null. + * + * - Optional parameter could be provided to instruct which previous RouterState to return, default one is 0. + * - 0 means the first before current. + * - 1 means the second before current. + * - 2 means the third before current. + * - 3 ... etc... + */ + getPrevious(index = 0): RouterState | null { + const lookupIndex = CollectionsUtil.isNumber(index) ? index : 0; + + if (lookupIndex >= 0 && lookupIndex < this.previousStates.length) { + return this.previousStates[lookupIndex]; + } + + return null; + } + + /** + * ** Append previous RouterState[] to current One. + * + * - Internal API used in reducer, not for public use. + */ + appendPrevious(routerState: RouterState): void { + const previousStoredStates: RouterState[] = [...routerState.previousStates]; + const cleanedPreviousState = RouterState.of( + routerState.state, + routerState.navigationId, + ); + + if (this.navigationId !== cleanedPreviousState.navigationId) { + if (previousStoredStates.length >= 10) { + previousStoredStates.pop(); + } + + previousStoredStates.unshift(cleanedPreviousState); + } + + this.previousStates.length = 0; + this.previousStates.push(...previousStoredStates); + } } /** * ** Route state serialized. */ interface SerializedRouteState { - url: string; - routePath: string; - absoluteRoutePath: string; - routePathSegments: string[]; - configPath: string; - absoluteConfigPath: string; - configPathSegments: string[]; - queryParams: { [key: string]: PrimitivesNil }; - routeSegments: RouteSegments; + url: string; + routePath: string; + absoluteRoutePath: string; + routePathSegments: string[]; + configPath: string; + absoluteConfigPath: string; + configPathSegments: string[]; + queryParams: { [key: string]: PrimitivesNil }; + routeSegments: RouteSegments; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts index fae88ecf3a..340e0ead79 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; -export * from './factory'; +export * from "./index"; +export * from "./factory"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts index 37caa38fa6..a46616ba8d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './router.service'; +export * from "./router.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts index a568e1f8a6..95263d0291 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts @@ -5,193 +5,204 @@ /* eslint-disable @typescript-eslint/dot-notation */ -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; -import { of } from 'rxjs'; -import { delay } from 'rxjs/operators'; +import { of } from "rxjs"; +import { delay } from "rxjs/operators"; -import { marbles } from 'rxjs-marbles/jasmine'; +import { marbles } from "rxjs-marbles/jasmine"; -import { Store } from '@ngrx/store'; +import { Store } from "@ngrx/store"; -import { STORE_ROUTER, StoreState } from '../../ngrx'; +import { STORE_ROUTER, StoreState } from "../../ngrx"; -import { RouterState, RouteState } from '../model'; +import { RouterState, RouteState } from "../model"; -import { RouterService, RouterServiceImpl } from './router.service'; +import { RouterService, RouterServiceImpl } from "./router.service"; -describe('RouterService -> RouterServiceImpl', () => { - let storeStub$: jasmine.SpyObj>; - let service: RouterService; +describe("RouterService -> RouterServiceImpl", () => { + let storeStub$: jasmine.SpyObj>; + let service: RouterService; - beforeEach(() => { - storeStub$ = jasmine.createSpyObj>('store', ['select']); + beforeEach(() => { + storeStub$ = jasmine.createSpyObj>("store", ["select"]); - TestBed.configureTestingModule({ - providers: [ - { provide: Store, useValue: storeStub$ }, - { provide: RouterService, useClass: RouterServiceImpl } - ] - }); - - service = TestBed.inject(RouterService); - }); - - it('should verify instance is created', () => { - // Then - expect(service).toBeDefined(); + TestBed.configureTestingModule({ + providers: [ + { provide: Store, useValue: storeStub$ }, + { provide: RouterService, useClass: RouterServiceImpl }, + ], }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|get|', () => { - it('should verify will return RouterState', () => { - // When - const routerState = RouterService.get(); + service = TestBed.inject(RouterService); + }); - // Then - expect(routerState.state).toEqual(RouteState.empty()); - }); - }); + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + }); - describe('|getState|', () => { - it('should verify will return RouteState', () => { - // When - const routeState = RouterService.getState(); + describe("Statics::", () => { + describe("Methods::", () => { + describe("|get|", () => { + it("should verify will return RouterState", () => { + // When + const routerState = RouterService.get(); - // Then - expect(routeState).toEqual(RouteState.empty()); - }); - }); + // Then + expect(routerState.state).toEqual(RouteState.empty()); }); - }); + }); - describe('Methods::', () => { - describe('|get|', () => { - it('should verify will select RouterState from Store', () => { - // Given - storeStub$.select.and.returnValue(of({ state: {}, navigationId: 5 } as RouterState)); + describe("|getState|", () => { + it("should verify will return RouteState", () => { + // When + const routeState = RouterService.getState(); - // When - service.get(); + // Then + expect(routeState).toEqual(RouteState.empty()); + }); + }); + }); + }); - // Then - expect(storeStub$.select.calls.mostRecent().args).toEqual([STORE_ROUTER]); - }); + describe("Methods::", () => { + describe("|get|", () => { + it("should verify will select RouterState from Store", () => { + // Given + storeStub$.select.and.returnValue( + of({ state: {}, navigationId: 5 } as RouterState), + ); - it( - 'should verify will return correct Observable state', - marbles((m) => { - // Given - const storeStream$ = m.cold('----a---', { - a: { - state: RouteState.empty(), - navigationId: 7 - } as RouterState - }); - const expectedOutputStream$ = m.cold('----a---', { - a: { - state: RouteState.empty(), - navigationId: 7 - } as RouterState - }); - storeStub$.select.and.returnValue(storeStream$); - - // When - const response$ = service.get(); - - // Then - m.expect(response$).toBeObservable(expectedOutputStream$); - }) - ); - }); + // When + service.get(); - describe('|getState|', () => { - it('should verify will select RouterState from Store', () => { - // Given - storeStub$.select.and.returnValue(of({ state: {}, navigationId: 5 } as RouterState)); + // Then + expect(storeStub$.select.calls.mostRecent().args).toEqual([ + STORE_ROUTER, + ]); + }); + + it( + "should verify will return correct Observable state", + marbles((m) => { + // Given + const storeStream$ = m.cold("----a---", { + a: { + state: RouteState.empty(), + navigationId: 7, + } as RouterState, + }); + const expectedOutputStream$ = m.cold("----a---", { + a: { + state: RouteState.empty(), + navigationId: 7, + } as RouterState, + }); + storeStub$.select.and.returnValue(storeStream$); + + // When + const response$ = service.get(); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + }), + ); + }); - // When - service.getState(); + describe("|getState|", () => { + it("should verify will select RouterState from Store", () => { + // Given + storeStub$.select.and.returnValue( + of({ state: {}, navigationId: 5 } as RouterState), + ); - // Then - expect(storeStub$.select.calls.mostRecent().args).toEqual([STORE_ROUTER]); - }); + // When + service.getState(); - it( - 'should verify will return correct Observable state', - marbles((m) => { - // Given - const storeStream$ = m.cold('----a---', { - a: { - state: RouteState.empty(), - navigationId: 7 - } as RouterState - }); - const expectedOutputStream$ = m.cold('----a---', { - a: RouteState.empty() - }); - storeStub$.select.and.returnValue(storeStream$); - - // When - const response$ = service.getState(); - - // Then - m.expect(response$).toBeObservable(expectedOutputStream$); - }) - ); - }); + // Then + expect(storeStub$.select.calls.mostRecent().args).toEqual([ + STORE_ROUTER, + ]); + }); + + it( + "should verify will return correct Observable state", + marbles((m) => { + // Given + const storeStream$ = m.cold("----a---", { + a: { + state: RouteState.empty(), + navigationId: 7, + } as RouterState, + }); + const expectedOutputStream$ = m.cold("----a---", { + a: RouteState.empty(), + }); + storeStub$.select.and.returnValue(storeStream$); + + // When + const response$ = service.getState(); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + }), + ); + }); - describe('|initialize|', () => { - it('should verify will push created subscriptions to buffer', () => { - // Given - const storeStream$ = of({ - state: RouteState.empty(), - navigationId: 7 - } as RouterState); - storeStub$.select.and.returnValue(storeStream$); - // @ts-ignore - const cleanSubSpy = spyOn(service, 'cleanSubscriptions').and.callThrough(); - - // Then 1 - expect(service['subscriptions'].length).toEqual(0); - - // When - service.initialize(); - - // Then 2 - expect(service['subscriptions'].length).toEqual(1); - expect(cleanSubSpy).toHaveBeenCalled(); + describe("|initialize|", () => { + it("should verify will push created subscriptions to buffer", () => { + // Given + const storeStream$ = of({ + state: RouteState.empty(), + navigationId: 7, + } as RouterState); + storeStub$.select.and.returnValue(storeStream$); + // @ts-ignore + const cleanSubSpy = spyOn( + service, + "cleanSubscriptions", + ).and.callThrough(); + + // Then 1 + expect(service["subscriptions"].length).toEqual(0); + + // When + service.initialize(); + + // Then 2 + expect(service["subscriptions"].length).toEqual(1); + expect(cleanSubSpy).toHaveBeenCalled(); + }); + + it( + "should verify will create subscriptions and will assign value to local state", + marbles((m) => { + // Given + const routerState = RouterState.of(RouteState.empty(), 7); + const storeStream$ = m.hot("-^--a---", { + a: routerState, + }); + storeStub$.select.and.returnValue(storeStream$); + + // When 1 + service.initialize(); + + // delay of 4 frames + of(true) + .pipe(delay(m.time("----|"))) + .subscribe(() => { + // Then 1 + expect(RouterService["_routerState"]).toBe(routerState); + + // When 2 // destroy Service + service.ngOnDestroy(); + + // Then + m.expect(storeStream$).toHaveSubscriptions("^---!"); }); - - it( - 'should verify will create subscriptions and will assign value to local state', - marbles((m) => { - // Given - const routerState = RouterState.of(RouteState.empty(), 7); - const storeStream$ = m.hot('-^--a---', { - a: routerState - }); - storeStub$.select.and.returnValue(storeStream$); - - // When 1 - service.initialize(); - - // delay of 4 frames - of(true) - .pipe(delay(m.time('----|'))) - .subscribe(() => { - // Then 1 - expect(RouterService['_routerState']).toBe(routerState); - - // When 2 // destroy Service - service.ngOnDestroy(); - - // Then - m.expect(storeStream$).toHaveSubscriptions('^---!'); - }); - }) - ); - }); + }), + ); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts index eff9989b5f..72f1cef6b6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts @@ -3,18 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Directive, Injectable } from '@angular/core'; +import { Directive, Injectable } from "@angular/core"; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; -import { Store } from '@ngrx/store'; +import { Store } from "@ngrx/store"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { STORE_ROUTER, StoreState } from '../../ngrx'; +import { STORE_ROUTER, StoreState } from "../../ngrx"; -import { RouterState, RouteState } from '../model'; +import { RouterState, RouteState } from "../model"; /** * ** Router Service. @@ -22,36 +22,36 @@ import { RouterState, RouteState } from '../model'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix export abstract class RouterService extends TaurusObject { - protected static _routerState: RouterState = RouterState.empty(); - - /** - * ** Will return current Router. - */ - static get(): RouterState { - return RouterService._routerState; - } - - /** - * ** Will return current Route State. - */ - static getState(): RouteState { - return RouterService._routerState.state; - } - - /** - * ** Will return Observable with NgRx Route State. - */ - abstract get(): Observable; - - /** - * ** Will return Observable with Route State. - */ - abstract getState(): Observable; - - /** - * ** Will initialize service. - */ - abstract initialize(): void; + protected static _routerState: RouterState = RouterState.empty(); + + /** + * ** Will return current Router. + */ + static get(): RouterState { + return RouterService._routerState; + } + + /** + * ** Will return current Route State. + */ + static getState(): RouteState { + return RouterService._routerState.state; + } + + /** + * ** Will return Observable with NgRx Route State. + */ + abstract get(): Observable; + + /** + * ** Will return Observable with Route State. + */ + abstract getState(): Observable; + + /** + * ** Will initialize service. + */ + abstract initialize(): void; } /** @@ -59,34 +59,34 @@ export abstract class RouterService extends TaurusObject { */ @Injectable() export class RouterServiceImpl extends RouterService { - constructor(private readonly store$: Store) { - super(); - } - - /** - * @inheritDoc - */ - get(): Observable { - return this.store$.select(STORE_ROUTER); - } - - /** - * @inheritDoc - */ - getState(): Observable { - return this.get().pipe(map((data) => data.state)); - } - - /** - * @inheritDoc - */ - initialize(): void { - this.cleanSubscriptions(); - - this.subscriptions.push( - this.get().subscribe((state) => { - RouterService._routerState = state; - }) - ); - } + constructor(private readonly store$: Store) { + super(); + } + + /** + * @inheritDoc + */ + get(): Observable { + return this.store$.select(STORE_ROUTER); + } + + /** + * @inheritDoc + */ + getState(): Observable { + return this.get().pipe(map((data) => data.state)); + } + + /** + * @inheritDoc + */ + initialize(): void { + this.cleanSubscriptions(); + + this.subscriptions.push( + this.get().subscribe((state) => { + RouterService._routerState = state; + }), + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts index 569b8697c7..d374a5af1f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './router.actions'; +export * from "./router.actions"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts index 4ff15d974d..f129ace21b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts @@ -3,186 +3,188 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { BaseAction, BaseActionWithPayload } from '../../../ngrx'; +import { BaseAction, BaseActionWithPayload } from "../../../ngrx"; import { - LOCATION_BACK, - LOCATION_FORWARD, - LOCATION_GO, - LocationBack, - LocationForward, - LocationGo, - ROUTER_NAVIGATE, - RouterNavigate -} from './router.actions'; - -describe('NavigationActions', () => { - describe('RouterNavigate', () => { - it('should verify instance is created', () => { - // Then - expect(() => new RouterNavigate({ commands: [], extras: {} })).toBeDefined(); - }); + LOCATION_BACK, + LOCATION_FORWARD, + LOCATION_GO, + LocationBack, + LocationForward, + LocationGo, + ROUTER_NAVIGATE, + RouterNavigate, +} from "./router.actions"; + +describe("NavigationActions", () => { + describe("RouterNavigate", () => { + it("should verify instance is created", () => { + // Then + expect( + () => new RouterNavigate({ commands: [], extras: {} }), + ).toBeDefined(); + }); - it('should verify correct type is assigned', () => { - // When - const instance = new RouterNavigate({ commands: [], extras: {} }); + it("should verify correct type is assigned", () => { + // When + const instance = new RouterNavigate({ commands: [], extras: {} }); - // Then - expect(instance.type).toEqual(ROUTER_NAVIGATE); - }); + // Then + expect(instance.type).toEqual(ROUTER_NAVIGATE); + }); + + it("should verify prototype chaining", () => { + // When + const instance = new RouterNavigate({ commands: [], extras: {} }); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new RouterNavigate({ commands: [], extras: {} }); + const instance = RouterNavigate.of({ commands: [], extras: {} }); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = RouterNavigate.of({ commands: [], extras: {} }); - - // Then - expect(instance).toBeInstanceOf(RouterNavigate); - }); - }); - }); + expect(instance).toBeInstanceOf(RouterNavigate); + }); }); + }); + }); + }); + + describe("LocationGo", () => { + it("should verify instance is created", () => { + // Then + expect( + () => + new LocationGo({ + path: "entity/15", + query: "search=test-team", + state: {}, + }), + ).toBeDefined(); }); - describe('LocationGo', () => { - it('should verify instance is created', () => { - // Then - expect( - () => - new LocationGo({ - path: 'entity/15', - query: 'search=test-team', - state: {} - }) - ).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new LocationGo({ + path: "entity/15", + query: "search=test-team", + state: {}, + }); - it('should verify correct type is assigned', () => { - // When - const instance = new LocationGo({ - path: 'entity/15', - query: 'search=test-team', - state: {} - }); + // Then + expect(instance.type).toEqual(LOCATION_GO); + }); - // Then - expect(instance.type).toEqual(LOCATION_GO); - }); + it("should verify prototype chaining", () => { + // When + const instance = new LocationGo({ + path: "entity/15", + query: "search=test-team", + state: {}, + }); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new LocationGo({ - path: 'entity/15', - query: 'search=test-team', - state: {} + const instance = LocationGo.of({ + path: "entity/15", + query: "search=test-team", + state: {}, }); // Then - expect(instance).toBeInstanceOf(BaseActionWithPayload); - expect(instance).toBeInstanceOf(BaseAction); + expect(instance).toBeInstanceOf(LocationGo); + }); }); + }); + }); + }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = LocationGo.of({ - path: 'entity/15', - query: 'search=test-team', - state: {} - }); - - // Then - expect(instance).toBeInstanceOf(LocationGo); - }); - }); - }); - }); + describe("LocationBack", () => { + it("should verify instance is created", () => { + // Then + expect(() => new LocationBack()).toBeDefined(); }); - describe('LocationBack', () => { - it('should verify instance is created', () => { - // Then - expect(() => new LocationBack()).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new LocationBack(); - it('should verify correct type is assigned', () => { - // When - const instance = new LocationBack(); + // Then + expect(instance.type).toEqual(LOCATION_BACK); + }); - // Then - expect(instance.type).toEqual(LOCATION_BACK); - }); + it("should verify prototype chaining", () => { + // When + const instance = new LocationBack(); + + // Then + expect(instance).toBeInstanceOf(BaseAction); + }); - it('should verify prototype chaining', () => { + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new LocationBack(); + const instance = LocationBack.of(); // Then - expect(instance).toBeInstanceOf(BaseAction); + expect(instance).toBeInstanceOf(LocationBack); + }); }); + }); + }); + }); - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = LocationBack.of(); - - // Then - expect(instance).toBeInstanceOf(LocationBack); - }); - }); - }); - }); + describe("LocationForward", () => { + it("should verify instance is created", () => { + // Then + expect(() => new LocationForward()).toBeDefined(); }); - describe('LocationForward', () => { - it('should verify instance is created', () => { - // Then - expect(() => new LocationForward()).toBeDefined(); - }); + it("should verify correct type is assigned", () => { + // When + const instance = new LocationForward(); - it('should verify correct type is assigned', () => { - // When - const instance = new LocationForward(); + // Then + expect(instance.type).toEqual(LOCATION_FORWARD); + }); - // Then - expect(instance.type).toEqual(LOCATION_FORWARD); - }); + it("should verify prototype chaining", () => { + // When + const instance = new LocationForward(); - it('should verify prototype chaining', () => { + // Then + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { // When - const instance = new LocationForward(); + const instance = LocationForward.of(); // Then - expect(instance).toBeInstanceOf(BaseAction); - }); - - describe('Statics::', () => { - describe('Methods::', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const instance = LocationForward.of(); - - // Then - expect(instance).toBeInstanceOf(LocationForward); - }); - }); - }); + expect(instance).toBeInstanceOf(LocationForward); + }); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts index ebe2cf5064..bf3cf44f9a 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts @@ -3,107 +3,111 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NavigationExtras } from '@angular/router'; +import { NavigationExtras } from "@angular/router"; -import { BaseAction, BaseActionWithPayload } from '../../../ngrx/actions'; +import { BaseAction, BaseActionWithPayload } from "../../../ngrx/actions"; /** * ** Action Identifier for Router Navigate. */ -export const ROUTER_NAVIGATE = '[router] Navigate'; +export const ROUTER_NAVIGATE = "[router] Navigate"; /** * ** Action Identifier for Location Go. * * */ -export const LOCATION_GO = '[location] Go'; +export const LOCATION_GO = "[location] Go"; /** * ** Action Identifier for Location Back. */ -export const LOCATION_BACK = '[location] Back'; +export const LOCATION_BACK = "[location] Back"; /** * ** Action Identifier for Location Forward. */ -export const LOCATION_FORWARD = '[location] Forward'; +export const LOCATION_FORWARD = "[location] Forward"; export interface NavigatePayload { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - commands: any[]; - extras?: NavigationExtras; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + commands: any[]; + extras?: NavigationExtras; } /** * ** Navigate Action instruct subscribers that they should navigate to given path. */ export class RouterNavigate extends BaseActionWithPayload { - constructor(payload: NavigatePayload) { - super(ROUTER_NAVIGATE, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: NavigatePayload) { - return new RouterNavigate(payload); - } + constructor(payload: NavigatePayload) { + super(ROUTER_NAVIGATE, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: NavigatePayload) { + return new RouterNavigate(payload); + } } export interface GoPayload { - path: string; - query?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state?: any; + path: string; + query?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state?: any; } /** * ** Location Go Action instruct subscribers that they should navigate using Location. */ export class LocationGo extends BaseActionWithPayload { - constructor(payload: GoPayload) { - super(LOCATION_GO, payload); - } - - /** - * ** Factory method. - */ - static override of(payload: GoPayload) { - return new LocationGo(payload); - } + constructor(payload: GoPayload) { + super(LOCATION_GO, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: GoPayload) { + return new LocationGo(payload); + } } /** * ** Back Action instruct subscribers to pop history Backward. */ export class LocationBack extends BaseAction { - constructor() { - super(LOCATION_BACK); - } - - /** - * ** Factory method. - */ - static of() { - return new LocationBack(); - } + constructor() { + super(LOCATION_BACK); + } + + /** + * ** Factory method. + */ + static of() { + return new LocationBack(); + } } /** * ** Forward Action instruct subscribers to go Forward. */ export class LocationForward extends BaseAction { - constructor() { - super(LOCATION_FORWARD); - } - - /** - * ** Factory method. - */ - static of() { - return new LocationForward(); - } + constructor() { + super(LOCATION_FORWARD); + } + + /** + * ** Factory method. + */ + static of() { + return new LocationForward(); + } } -export type NavigationActions = RouterNavigate | LocationGo | LocationBack | LocationForward; +export type NavigationActions = + | RouterNavigate + | LocationGo + | LocationBack + | LocationForward; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts index a7f6a1382c..6315f98ecb 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './router.effects'; +export * from "./router.effects"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts index fefb75b544..5aa90ff1ce 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts @@ -3,145 +3,156 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Location } from '@angular/common'; -import { Router } from '@angular/router'; -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { provideMockActions } from '@ngrx/effects/testing'; - -import { Observable } from 'rxjs'; -import { marbles } from 'rxjs-marbles/jasmine'; - -import { RouterEffects } from './router.effects'; -import { LocationBack, LocationForward, LocationGo, RouterNavigate } from '../actions'; - -describe('RouterEffects', () => { - let effects: RouterEffects; - - let routeAction$: Observable; - - let routerStub: jasmine.SpyObj; - let locationStub: jasmine.SpyObj; - - beforeEach(waitForAsync(() => { - routerStub = jasmine.createSpyObj('router', ['navigate']); - locationStub = jasmine.createSpyObj('location', ['go', 'back', 'forward']); - - TestBed.configureTestingModule({ - providers: [ - { - provide: Router, - useValue: routerStub - }, - { - provide: Location, - useValue: locationStub - }, - provideMockActions(() => routeAction$), - RouterEffects - ] - }); - - routerStub.navigate.and.returnValue(Promise.resolve(true)); - - effects = TestBed.inject(RouterEffects); - })); - - it( - 'should use Location.go()', - marbles((m) => { - routeAction$ = m.hot('-a', { a: new LocationGo({ path: '/test', query: 'search=random', state: {} }) }); - effects.locationGo$.subscribe(() => { - expect(locationStub.go).toHaveBeenCalled(); - }); - }) - ); - - it( - `should verify will handle error and won't invoke Location.go()`, - marbles((m) => { - routeAction$ = m.hot('-#'); - - effects.locationGo$.subscribe((v) => { - expect(v).toBeTrue(); - expect(locationStub.go).not.toHaveBeenCalled(); - }); - }) - ); - - it( - 'should navigate back', - marbles((m) => { - routeAction$ = m.hot('-a', { a: new LocationBack() }); - effects.locationBack$.subscribe(() => { - expect(locationStub.back).toHaveBeenCalled(); - }); - }) - ); - - it( - `should verify will handle error and won't invoke navigate back`, - marbles((m) => { - routeAction$ = m.hot('-#'); - - effects.locationBack$.subscribe((v) => { - expect(v).toBeTrue(); - expect(locationStub.back).not.toHaveBeenCalled(); - }); - }) - ); - - it( - 'should navigate forward', - marbles((m) => { - routeAction$ = m.hot('-a', { a: new LocationForward() }); - effects.locationForward$.subscribe(() => { - expect(locationStub.forward).toHaveBeenCalled(); - }); - }) - ); - - it( - `should verify will handle error and won't invoke Location.forward()`, - marbles((m) => { - routeAction$ = m.hot('-#'); - - effects.locationForward$.subscribe((v) => { - expect(v).toBeTrue(); - expect(locationStub.forward).not.toHaveBeenCalled(); - }); - }) - ); - - it( - 'should navigate to path', - marbles((m) => { - const goAction = new RouterNavigate({ - commands: ['/domain', 'context', 'entity', 10, 'sub-entity', 7], - extras: { - queryParams: { value: 1 } - } - }); - - routeAction$ = m.hot('a', { a: goAction }); - effects.routerNavigate$.subscribe(() => { - expect(routerStub.navigate).toHaveBeenCalledWith( - ['/domain', 'context', 'entity', 10, 'sub-entity', 7], - goAction.payload.extras - ); - }); - }) - ); - - it( - `should verify will handle error and won't invoke router navigate`, - marbles((m) => { - routeAction$ = m.hot('-#'); - - effects.routerNavigate$.subscribe((v) => { - expect(v).toBeTrue(); - expect(routerStub.navigate).not.toHaveBeenCalled(); - }); - }) - ); +import { Location } from "@angular/common"; +import { Router } from "@angular/router"; +import { TestBed, waitForAsync } from "@angular/core/testing"; + +import { provideMockActions } from "@ngrx/effects/testing"; + +import { Observable } from "rxjs"; +import { marbles } from "rxjs-marbles/jasmine"; + +import { RouterEffects } from "./router.effects"; +import { + LocationBack, + LocationForward, + LocationGo, + RouterNavigate, +} from "../actions"; + +describe("RouterEffects", () => { + let effects: RouterEffects; + + let routeAction$: Observable; + + let routerStub: jasmine.SpyObj; + let locationStub: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + routerStub = jasmine.createSpyObj("router", ["navigate"]); + locationStub = jasmine.createSpyObj("location", [ + "go", + "back", + "forward", + ]); + + TestBed.configureTestingModule({ + providers: [ + { + provide: Router, + useValue: routerStub, + }, + { + provide: Location, + useValue: locationStub, + }, + provideMockActions(() => routeAction$), + RouterEffects, + ], + }); + + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + effects = TestBed.inject(RouterEffects); + })); + + it( + "should use Location.go()", + marbles((m) => { + routeAction$ = m.hot("-a", { + a: new LocationGo({ path: "/test", query: "search=random", state: {} }), + }); + effects.locationGo$.subscribe(() => { + expect(locationStub.go).toHaveBeenCalled(); + }); + }), + ); + + it( + `should verify will handle error and won't invoke Location.go()`, + marbles((m) => { + routeAction$ = m.hot("-#"); + + effects.locationGo$.subscribe((v) => { + expect(v).toBeTrue(); + expect(locationStub.go).not.toHaveBeenCalled(); + }); + }), + ); + + it( + "should navigate back", + marbles((m) => { + routeAction$ = m.hot("-a", { a: new LocationBack() }); + effects.locationBack$.subscribe(() => { + expect(locationStub.back).toHaveBeenCalled(); + }); + }), + ); + + it( + `should verify will handle error and won't invoke navigate back`, + marbles((m) => { + routeAction$ = m.hot("-#"); + + effects.locationBack$.subscribe((v) => { + expect(v).toBeTrue(); + expect(locationStub.back).not.toHaveBeenCalled(); + }); + }), + ); + + it( + "should navigate forward", + marbles((m) => { + routeAction$ = m.hot("-a", { a: new LocationForward() }); + effects.locationForward$.subscribe(() => { + expect(locationStub.forward).toHaveBeenCalled(); + }); + }), + ); + + it( + `should verify will handle error and won't invoke Location.forward()`, + marbles((m) => { + routeAction$ = m.hot("-#"); + + effects.locationForward$.subscribe((v) => { + expect(v).toBeTrue(); + expect(locationStub.forward).not.toHaveBeenCalled(); + }); + }), + ); + + it( + "should navigate to path", + marbles((m) => { + const goAction = new RouterNavigate({ + commands: ["/domain", "context", "entity", 10, "sub-entity", 7], + extras: { + queryParams: { value: 1 }, + }, + }); + + routeAction$ = m.hot("a", { a: goAction }); + effects.routerNavigate$.subscribe(() => { + expect(routerStub.navigate).toHaveBeenCalledWith( + ["/domain", "context", "entity", 10, "sub-entity", 7], + goAction.payload.extras, + ); + }); + }), + ); + + it( + `should verify will handle error and won't invoke router navigate`, + marbles((m) => { + routeAction$ = m.hot("-#"); + + effects.routerNavigate$.subscribe((v) => { + expect(v).toBeTrue(); + expect(routerStub.navigate).not.toHaveBeenCalled(); + }); + }), + ); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts index b34723aca2..c06c38cb38 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts @@ -3,96 +3,106 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; -import { Location } from '@angular/common'; -import { Router } from '@angular/router'; +import { Injectable } from "@angular/core"; +import { Location } from "@angular/common"; +import { Router } from "@angular/router"; -import { Observable, of } from 'rxjs'; -import { catchError, map, tap } from 'rxjs/operators'; +import { Observable, of } from "rxjs"; +import { catchError, map, tap } from "rxjs/operators"; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Actions, createEffect, ofType } from "@ngrx/effects"; -import { GoPayload, LOCATION_BACK, LOCATION_FORWARD, LOCATION_GO, NavigatePayload, ROUTER_NAVIGATE, RouterNavigate } from '../actions'; +import { + GoPayload, + LOCATION_BACK, + LOCATION_FORWARD, + LOCATION_GO, + NavigatePayload, + ROUTER_NAVIGATE, + RouterNavigate, +} from "../actions"; /** * ** Router Effects Service. */ @Injectable() export class RouterEffects { - /** - * ** Effect for Router navigation. - */ - routerNavigate$ = createEffect( - () => - this.actions$.pipe( - ofType(ROUTER_NAVIGATE), - map((action: RouterNavigate) => action.payload), - tap((payload) => this._navigate(payload)), - catchError((error: unknown) => RouterEffects._handleError(error)) - ), - { dispatch: false } - ); + /** + * ** Effect for Router navigation. + */ + routerNavigate$ = createEffect( + () => + this.actions$.pipe( + ofType(ROUTER_NAVIGATE), + map((action: RouterNavigate) => action.payload), + tap((payload) => this._navigate(payload)), + catchError((error: unknown) => RouterEffects._handleError(error)), + ), + { dispatch: false }, + ); - /** - * ** Effect for Location go (navigate). - */ - locationGo$ = createEffect( - () => - this.actions$.pipe( - ofType(LOCATION_GO), - tap((payload: GoPayload) => this.location.go(payload.path, payload.query, payload.state)), - catchError((error: unknown) => RouterEffects._handleError(error)) - ), - { dispatch: false } - ); + /** + * ** Effect for Location go (navigate). + */ + locationGo$ = createEffect( + () => + this.actions$.pipe( + ofType(LOCATION_GO), + tap((payload: GoPayload) => + this.location.go(payload.path, payload.query, payload.state), + ), + catchError((error: unknown) => RouterEffects._handleError(error)), + ), + { dispatch: false }, + ); - /** - * ** Effect for pop Backward Browser state. - */ - locationBack$ = createEffect( - () => - this.actions$.pipe( - ofType(LOCATION_BACK), - tap(() => this.location.back()), - catchError((error: unknown) => RouterEffects._handleError(error)) - ), - { dispatch: false } - ); + /** + * ** Effect for pop Backward Browser state. + */ + locationBack$ = createEffect( + () => + this.actions$.pipe( + ofType(LOCATION_BACK), + tap(() => this.location.back()), + catchError((error: unknown) => RouterEffects._handleError(error)), + ), + { dispatch: false }, + ); - /** - * ** Effect for push Forward Browser state. - */ - locationForward$ = createEffect( - () => - this.actions$.pipe( - ofType(LOCATION_FORWARD), - tap(() => this.location.forward()), - catchError((error: unknown) => RouterEffects._handleError(error)) - ), - { dispatch: false } - ); + /** + * ** Effect for push Forward Browser state. + */ + locationForward$ = createEffect( + () => + this.actions$.pipe( + ofType(LOCATION_FORWARD), + tap(() => this.location.forward()), + catchError((error: unknown) => RouterEffects._handleError(error)), + ), + { dispatch: false }, + ); - /** - * ** Constructor. - */ - constructor( - private readonly actions$: Actions, - private readonly router: Router, - private readonly location: Location - ) {} + /** + * ** Constructor. + */ + constructor( + private readonly actions$: Actions, + private readonly router: Router, + private readonly location: Location, + ) {} - private static _handleError(error: unknown): Observable { - console.error(error); + private static _handleError(error: unknown): Observable { + console.error(error); - return of(true); - } + return of(true); + } - private _navigate(payload: NavigatePayload): void { - const extras = payload.extras ?? {}; + private _navigate(payload: NavigatePayload): void { + const extras = payload.extras ?? {}; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(payload.commands, extras).then(() => { - // No-op. - }); - } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(payload.commands, extras).then(() => { + // No-op. + }); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts index b2bdb94940..b17f2500fe 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './router.reducer'; +export * from "./router.reducer"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts index b9e9167576..a19e9d2b1b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts @@ -3,34 +3,52 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RouterStateSnapshot, RoutesRecognized } from '@angular/router'; +import { RouterStateSnapshot, RoutesRecognized } from "@angular/router"; -import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store'; +import { ROUTER_NAVIGATION, RouterNavigationAction } from "@ngrx/router-store"; -import { RouterState, RouteState } from '../../model'; +import { RouterState, RouteState } from "../../model"; -import { routerReducer } from './router.reducer'; +import { routerReducer } from "./router.reducer"; -describe('routerReducer', () => { - it('should verify will invoke correct methods', () => { - // Given - const navigateAction: RouterNavigationAction = { - type: ROUTER_NAVIGATION, - payload: { - routerState: RouteState.empty(), - event: new RoutesRecognized(5, 'domain/context', 'domain/context', {} as RouterStateSnapshot) - } - }; - const storedRouterStateStub = jasmine.createSpyObj('routerState', ['appendPrevious']); - const newRouterStateStub = jasmine.createSpyObj('routerState', ['appendPrevious']); - const factoryMethodSpy = spyOn(RouterState, 'of').and.returnValue(newRouterStateStub); +describe("routerReducer", () => { + it("should verify will invoke correct methods", () => { + // Given + const navigateAction: RouterNavigationAction = { + type: ROUTER_NAVIGATION, + payload: { + routerState: RouteState.empty(), + event: new RoutesRecognized( + 5, + "domain/context", + "domain/context", + {} as RouterStateSnapshot, + ), + }, + }; + const storedRouterStateStub = jasmine.createSpyObj( + "routerState", + ["appendPrevious"], + ); + const newRouterStateStub = jasmine.createSpyObj( + "routerState", + ["appendPrevious"], + ); + const factoryMethodSpy = spyOn(RouterState, "of").and.returnValue( + newRouterStateStub, + ); - // When - const value = routerReducer(storedRouterStateStub, navigateAction); + // When + const value = routerReducer(storedRouterStateStub, navigateAction); - // Then - expect(value).toBe(newRouterStateStub); - expect(factoryMethodSpy).toHaveBeenCalledWith(navigateAction.payload.routerState, navigateAction.payload.event.id); - expect(newRouterStateStub.appendPrevious).toHaveBeenCalledWith(storedRouterStateStub); - }); + // Then + expect(value).toBe(newRouterStateStub); + expect(factoryMethodSpy).toHaveBeenCalledWith( + navigateAction.payload.routerState, + navigateAction.payload.event.id, + ); + expect(newRouterStateStub.appendPrevious).toHaveBeenCalledWith( + storedRouterStateStub, + ); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts index 4ce297a2d5..5874378094 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts @@ -6,36 +6,42 @@ /* eslint-disable arrow-body-style,prefer-arrow/prefer-arrow-functions */ import { - ROUTER_CANCEL, - ROUTER_ERROR, - ROUTER_NAVIGATION, - RouterCancelAction, - RouterErrorAction, - RouterNavigationAction -} from '@ngrx/router-store'; + ROUTER_CANCEL, + ROUTER_ERROR, + ROUTER_NAVIGATION, + RouterCancelAction, + RouterErrorAction, + RouterNavigationAction, +} from "@ngrx/router-store"; -import { RouterState, RouteState } from '../../model'; +import { RouterState, RouteState } from "../../model"; type AcceptedActions = - | RouterNavigationAction - | RouterErrorAction - | RouterCancelAction; + | RouterNavigationAction + | RouterErrorAction + | RouterCancelAction; /** * ** Reducer for Router Actions. */ -export function routerReducer(state = RouterState.empty(), action: AcceptedActions = { type: null, payload: null }) { - const actionPayload = action.payload; +export function routerReducer( + state = RouterState.empty(), + action: AcceptedActions = { type: null, payload: null }, +) { + const actionPayload = action.payload; - switch (action.type) { - case ROUTER_NAVIGATION: - case ROUTER_ERROR: - case ROUTER_CANCEL: - const newState = RouterState.of(actionPayload.routerState, actionPayload.event.id); - newState.appendPrevious(state); + switch (action.type) { + case ROUTER_NAVIGATION: + case ROUTER_ERROR: + case ROUTER_CANCEL: + const newState = RouterState.of( + actionPayload.routerState, + actionPayload.event.id, + ); + newState.appendPrevious(state); - return newState; - default: - return state; - } + return newState; + default: + return state; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts index 75e211e3d5..2a417f2373 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts @@ -16,14 +16,14 @@ @typescript-eslint/no-unsafe-assignment */ -import 'reflect-metadata'; +import "reflect-metadata"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { SystemEventMetadataRecords } from '../dispatcher/models'; -import { SystemEventHandlerRegistry } from '../dispatcher/registry'; +import { SystemEventMetadataRecords } from "../dispatcher/models"; +import { SystemEventHandlerRegistry } from "../dispatcher/registry"; -import { SYSTEM_EVENTS_METADATA_KEY } from './models'; +import { SYSTEM_EVENTS_METADATA_KEY } from "./models"; /** * ** Decorator to annotate Handler source Class as SystemEvent Handler source Class. @@ -46,71 +46,94 @@ import { SYSTEM_EVENTS_METADATA_KEY } from './models'; * } */ export function SystemEventHandlerClass(): ClassDecorator { - // @ts-ignore - return InstanceType>( - OriginConstructor: T - ): ((...args: ConstructorParameters) => InstanceType) => { - const originMetadataKeys = Reflect.getMetadataKeys(OriginConstructor); - const originMetadataRecords: SystemEventMetadataRecords = Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, OriginConstructor) ?? []; - const originOnDestroyRef: () => void = getOriginOnDestroyRef(OriginConstructor); - - let instance: InstanceType; - - const OverriddenConstructor = function (...args: ConstructorParameters): InstanceType { - instance = new OriginConstructor(...args); - - originMetadataRecords.forEach((r) => { - SystemEventHandlerRegistry.register>(r.events, r.handler, instance, r.filterExpression); - }); - - return instance; - }; + // @ts-ignore + return InstanceType>( + OriginConstructor: T, + ): ((...args: ConstructorParameters) => InstanceType) => { + const originMetadataKeys = Reflect.getMetadataKeys(OriginConstructor); + const originMetadataRecords: SystemEventMetadataRecords = + Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, OriginConstructor) ?? []; + const originOnDestroyRef: () => void = + getOriginOnDestroyRef(OriginConstructor); + + let instance: InstanceType; + + const OverriddenConstructor = function ( + ...args: ConstructorParameters + ): InstanceType { + instance = new OriginConstructor(...args); + + originMetadataRecords.forEach((r) => { + SystemEventHandlerRegistry.register>( + r.events, + r.handler, + instance, + r.filterExpression, + ); + }); + + return instance; + }; - OriginConstructor.prototype.ngOnDestroy = () => { - originMetadataRecords.forEach((r) => { - SystemEventHandlerRegistry.unregister(r.events, r.handler); + OriginConstructor.prototype.ngOnDestroy = () => { + originMetadataRecords.forEach((r) => { + SystemEventHandlerRegistry.unregister(r.events, r.handler); - if (originOnDestroyRef) { - originOnDestroyRef.call(instance); - } - }); - }; + if (originOnDestroyRef) { + originOnDestroyRef.call(instance); + } + }); + }; - originMetadataKeys.forEach((key) => { - Reflect.defineMetadata(key, Reflect.getMetadata(key, OriginConstructor), OverriddenConstructor); - }); + originMetadataKeys.forEach((key) => { + Reflect.defineMetadata( + key, + Reflect.getMetadata(key, OriginConstructor), + OverriddenConstructor, + ); + }); - copyFromOriginToOverride(OriginConstructor, OverriddenConstructor); + copyFromOriginToOverride(OriginConstructor, OverriddenConstructor); - return OverriddenConstructor; - }; + return OverriddenConstructor; + }; } -const getOriginOnDestroyRef = InstanceType>(classRef: T): (() => void | null) => { - if (!classRef || !classRef.prototype) { - return null; - } +const getOriginOnDestroyRef = < + T extends new (...args: any[]) => InstanceType, +>( + classRef: T, +): (() => void | null) => { + if (!classRef || !classRef.prototype) { + return null; + } - if (classRef.prototype.ngOnDestroy) { - return classRef.prototype.ngOnDestroy as () => void; - } + if (classRef.prototype.ngOnDestroy) { + return classRef.prototype.ngOnDestroy as () => void; + } - return getOriginOnDestroyRef(classRef.prototype); + return getOriginOnDestroyRef(classRef.prototype); }; -const copyFromOriginToOverride = InstanceType>( - OriginConstructor: T, - OverriddenConstructor: (...args: ConstructorParameters) => InstanceType +const copyFromOriginToOverride = < + T extends new (...args: any[]) => InstanceType, +>( + OriginConstructor: T, + OverriddenConstructor: (...args: ConstructorParameters) => InstanceType, ) => { - CollectionsUtil.iterateClassStatics(OriginConstructor, (descriptor, key) => { - const overriddenPropertyDescriptor = CollectionsUtil.getObjectPropertyDescriptor(OverriddenConstructor, key); - if (overriddenPropertyDescriptor && !overriddenPropertyDescriptor.writable) { - return; - } + CollectionsUtil.iterateClassStatics(OriginConstructor, (descriptor, key) => { + const overriddenPropertyDescriptor = + CollectionsUtil.getObjectPropertyDescriptor(OverriddenConstructor, key); + if ( + overriddenPropertyDescriptor && + !overriddenPropertyDescriptor.writable + ) { + return; + } - OverriddenConstructor[key] = descriptor.value; - }); + OverriddenConstructor[key] = descriptor.value; + }); - OverriddenConstructor.prototype = Object.create(OriginConstructor.prototype); - OverriddenConstructor.prototype.constructor = OverriddenConstructor; + OverriddenConstructor.prototype = Object.create(OriginConstructor.prototype); + OverriddenConstructor.prototype.constructor = OverriddenConstructor; }; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts index 7dcf3c0229..6e3c918568 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts @@ -3,65 +3,77 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SystemEventHandler } from './event-handler.decorator'; -import { SYSTEM_EVENTS_METADATA_KEY } from './models'; +import { SystemEventHandler } from "./event-handler.decorator"; +import { SYSTEM_EVENTS_METADATA_KEY } from "./models"; -describe('SystemEventHandler', () => { - let event: string; - let event2: string; - // eslint-disable-next-line @typescript-eslint/ban-types - let constructor: Function; - // eslint-disable-next-line @typescript-eslint/ban-types - let target: { constructor: Function }; - let propertyKey: string; - let propertyKey2: string; - let descriptor: PropertyDescriptor; - let descriptor2: PropertyDescriptor; +describe("SystemEventHandler", () => { + let event: string; + let event2: string; + // eslint-disable-next-line @typescript-eslint/ban-types + let constructor: Function; + // eslint-disable-next-line @typescript-eslint/ban-types + let target: { constructor: Function }; + let propertyKey: string; + let propertyKey2: string; + let descriptor: PropertyDescriptor; + let descriptor2: PropertyDescriptor; - beforeEach(() => { - class TestClazz {} + beforeEach(() => { + class TestClazz {} - event = 'SE_Register_User'; - event2 = 'SE_Unregister_User'; - constructor = TestClazz.prototype.constructor; - target = { constructor }; - propertyKey = '_eventHandlerRegister'; - propertyKey2 = 'eventHandleUnregister'; - descriptor = { value: () => Promise.resolve(true) }; - descriptor2 = { value: () => Promise.resolve(true) }; - }); + event = "SE_Register_User"; + event2 = "SE_Unregister_User"; + constructor = TestClazz.prototype.constructor; + target = { constructor }; + propertyKey = "_eventHandlerRegister"; + propertyKey2 = "eventHandleUnregister"; + descriptor = { value: () => Promise.resolve(true) }; + descriptor2 = { value: () => Promise.resolve(true) }; + }); - it('should verify will record metadata with Reflect to Constructor', () => { - // Then 1 - expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeFalse(); + it("should verify will record metadata with Reflect to Constructor", () => { + // Then 1 + expect( + Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor), + ).toBeFalse(); - // When - SystemEventHandler(event, null)(target, propertyKey, descriptor); + // When + SystemEventHandler(event, null)(target, propertyKey, descriptor); - // Then 2 - expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeTrue(); - expect(Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toEqual([ - { - handler: descriptor.value, - events: event, - filterExpression: null - } - ]); - }); + // Then 2 + expect( + Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor), + ).toBeTrue(); + expect( + Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor), + ).toEqual([ + { + handler: descriptor.value, + events: event, + filterExpression: null, + }, + ]); + }); - it('should verify will add metadata on existing with Reflect to Constructor', () => { - // Then 1 - expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeFalse(); + it("should verify will add metadata on existing with Reflect to Constructor", () => { + // Then 1 + expect( + Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor), + ).toBeFalse(); - // When - SystemEventHandler(event, null)(target, propertyKey, descriptor); - SystemEventHandler(event2, null)(target, propertyKey2, descriptor2); + // When + SystemEventHandler(event, null)(target, propertyKey, descriptor); + SystemEventHandler(event2, null)(target, propertyKey2, descriptor2); - // Then 2 - expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeTrue(); - expect(Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toEqual([ - { handler: descriptor.value, events: event, filterExpression: null }, - { handler: descriptor2.value, events: event2, filterExpression: null } - ]); - }); + // Then 2 + expect( + Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor), + ).toBeTrue(); + expect( + Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor), + ).toEqual([ + { handler: descriptor.value, events: event, filterExpression: null }, + { handler: descriptor2.value, events: event2, filterExpression: null }, + ]); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts index 352fd5ecb1..fbc09518a9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts @@ -8,14 +8,17 @@ max-len */ -import 'reflect-metadata'; +import "reflect-metadata"; -import { Expression } from '../../../common'; +import { Expression } from "../../../common"; -import { SystemEvent } from '../event'; -import { SystemEventHandlerRef, SystemEventMetadataRecords } from '../dispatcher/models'; +import { SystemEvent } from "../event"; +import { + SystemEventHandlerRef, + SystemEventMetadataRecords, +} from "../dispatcher/models"; -import { SYSTEM_EVENTS_METADATA_KEY } from './models'; +import { SYSTEM_EVENTS_METADATA_KEY } from "./models"; /** * ** Decorator to register method as SystemEvent Handler. @@ -99,34 +102,49 @@ import { SYSTEM_EVENTS_METADATA_KEY } from './models'; * } * } */ -export function SystemEventHandler(knownEvents: SystemEvent | SystemEvent[], filterExpression?: Expression): MethodDecorator { - return (target, propertyKey, descriptor) => { - if (!descriptor || !knownEvents) { - return; - } +export function SystemEventHandler( + knownEvents: SystemEvent | SystemEvent[], + filterExpression?: Expression, +): MethodDecorator { + return (target, propertyKey, descriptor) => { + if (!descriptor || !knownEvents) { + return; + } - let metadataRecords: SystemEventMetadataRecords = []; + let metadataRecords: SystemEventMetadataRecords = []; - if (Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, target.constructor)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - metadataRecords = Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, target.constructor); - metadataRecords.push({ - handler: descriptor.value as unknown as SystemEventHandlerRef, - events: knownEvents, - filterExpression - }); + if (Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, target.constructor)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + metadataRecords = Reflect.getMetadata( + SYSTEM_EVENTS_METADATA_KEY, + target.constructor, + ); + metadataRecords.push({ + handler: descriptor.value as unknown as SystemEventHandlerRef, + events: knownEvents, + filterExpression, + }); - Reflect.defineMetadata(SYSTEM_EVENTS_METADATA_KEY, metadataRecords, target.constructor, propertyKey); + Reflect.defineMetadata( + SYSTEM_EVENTS_METADATA_KEY, + metadataRecords, + target.constructor, + propertyKey, + ); - return; - } + return; + } - metadataRecords.push({ - handler: descriptor.value as unknown as SystemEventHandlerRef, - events: knownEvents, - filterExpression - }); + metadataRecords.push({ + handler: descriptor.value as unknown as SystemEventHandlerRef, + events: knownEvents, + filterExpression, + }); - Reflect.defineMetadata(SYSTEM_EVENTS_METADATA_KEY, metadataRecords, target.constructor); - }; + Reflect.defineMetadata( + SYSTEM_EVENTS_METADATA_KEY, + metadataRecords, + target.constructor, + ); + }; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts index 7490567946..e53d3b66d7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event-handler-class.decorator'; -export * from './event-handler.decorator'; +export * from "./event-handler-class.decorator"; +export * from "./event-handler.decorator"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts index 2f346006bb..02b71e90ce 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts @@ -6,4 +6,5 @@ /** * ** Key identifier for Taurus System Events Metadata on Class and Methods. */ -export const SYSTEM_EVENTS_METADATA_KEY = '__taurus::shared::system::events::metadata__'; +export const SYSTEM_EVENTS_METADATA_KEY = + "__taurus::shared::system::events::metadata__"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts index 6d9fc888fc..319f179649 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event-decorator-helper'; +export * from "./event-decorator-helper"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts index 59b6033515..c390575a36 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts @@ -3,132 +3,208 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { fakeAsync, tick } from '@angular/core/testing'; - -import { SE_NAVIGATE } from '../event'; - -import { SystemEventHandlerRegistry } from './registry'; -import { SystemEventDispatcher } from './event.dispatcher'; -import { SystemEventHandlerRef } from './models'; - -describe('SystemEventDispatcher', () => { - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|post|', () => { - it('should verify will post NON-BLOCKING System Event to 2 Handlers', fakeAsync(() => { - // Given - const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); - const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); - const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ - { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, - { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } - ]); - const spyRegistryFind = spyOn(SystemEventHandlerRegistry, 'findHandlerByReference').and.returnValue({ - handlerRef: () => Promise.resolve(true), - handlerRecord: { - handlerRef: () => Promise.resolve(true), - handlerClassInstance: null, - handlerFilterExpression: null - }, - active: true - }); - const payload = {}; - - // When - SystemEventDispatcher.post(SE_NAVIGATE, payload); - - // Then - tick(100); - expect(spyRegistryPrepared).toHaveBeenCalled(); - expect(spyHandler1.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); - expect(spyHandler2.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); - expect(spyRegistryFind).toHaveBeenCalledTimes(2); - })); - - it('should verify will post NON-BLOCKING System Event to 1 Handler and will skip the second one', fakeAsync(() => { - // Given - const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); - const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); - const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ - { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, - { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } - ]); - const spyRegistryFind = spyOn(SystemEventHandlerRegistry, 'findHandlerByReference').and.returnValue({ - handlerRef: () => Promise.resolve(true), - handlerRecord: { - handlerRef: () => Promise.resolve(true), - handlerClassInstance: null, - handlerFilterExpression: null - }, - active: true - }); - const payload = {}; - - // When - SystemEventDispatcher.post(SE_NAVIGATE, payload, 1); - - // Then - tick(100); - expect(spyRegistryPrepared).toHaveBeenCalled(); - expect(spyHandler1.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); - expect(spyHandler2.evaluate).not.toHaveBeenCalled(); - expect(spyRegistryFind).toHaveBeenCalledTimes(1); - })); - }); - - describe('|send|', () => { - it('should verify will send BLOCKING System Event to 2 Handlers', fakeAsync(() => { - // Given - const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); - const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); - spyHandler1.evaluate.and.returnValue(Promise.resolve(true)); - spyHandler2.evaluate.and.returnValue(Promise.resolve(true)); - - const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ - { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, - { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } - ]); - const payload = {}; - - // When - SystemEventDispatcher.send(SE_NAVIGATE, payload).then(() => { - // No-op. - }); - - tick(100); - - // Then - expect(spyRegistryPrepared).toHaveBeenCalled(); - expect(spyHandler1.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); - expect(spyHandler2.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); - })); - - it('should verify will send BLOCKING System Event to 1 Handler and will skip the second one', fakeAsync(() => { - // Given - const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); - const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); - spyHandler1.evaluate.and.returnValue(Promise.resolve(true)); - spyHandler2.evaluate.and.returnValue(Promise.resolve(true)); - - const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ - { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, - { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } - ]); - const payload = {}; - - // When - SystemEventDispatcher.send(SE_NAVIGATE, payload, 1).then(() => { - // No-op. - }); - - tick(100); - - // Then - expect(spyRegistryPrepared).toHaveBeenCalled(); - expect(spyHandler1.evaluate).not.toHaveBeenCalled(); - expect(spyHandler2.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); - })); - }); - }); +import { fakeAsync, tick } from "@angular/core/testing"; + +import { SE_NAVIGATE } from "../event"; + +import { SystemEventHandlerRegistry } from "./registry"; +import { SystemEventDispatcher } from "./event.dispatcher"; +import { SystemEventHandlerRef } from "./models"; + +describe("SystemEventDispatcher", () => { + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|post|", () => { + it("should verify will post NON-BLOCKING System Event to 2 Handlers", fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler1", ["evaluate"]); + const spyHandler2: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler2", ["evaluate"]); + const spyRegistryPrepared = spyOn( + SystemEventHandlerRegistry, + "getPreparedArrayOfHandlers", + ).and.returnValue([ + { + handlerRef: spyHandler1.evaluate, + handlerClassInstance: spyHandler1, + }, + { + handlerRef: spyHandler2.evaluate, + handlerClassInstance: spyHandler2, + }, + ]); + const spyRegistryFind = spyOn( + SystemEventHandlerRegistry, + "findHandlerByReference", + ).and.returnValue({ + handlerRef: () => Promise.resolve(true), + handlerRecord: { + handlerRef: () => Promise.resolve(true), + handlerClassInstance: null, + handlerFilterExpression: null, + }, + active: true, + }); + const payload = {}; + + // When + SystemEventDispatcher.post(SE_NAVIGATE, payload); + + // Then + tick(100); + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).toHaveBeenCalledWith( + payload, + SE_NAVIGATE, + ); + expect(spyHandler2.evaluate).toHaveBeenCalledWith( + payload, + SE_NAVIGATE, + ); + expect(spyRegistryFind).toHaveBeenCalledTimes(2); + })); + + it("should verify will post NON-BLOCKING System Event to 1 Handler and will skip the second one", fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler1", ["evaluate"]); + const spyHandler2: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler2", ["evaluate"]); + const spyRegistryPrepared = spyOn( + SystemEventHandlerRegistry, + "getPreparedArrayOfHandlers", + ).and.returnValue([ + { + handlerRef: spyHandler1.evaluate, + handlerClassInstance: spyHandler1, + }, + { + handlerRef: spyHandler2.evaluate, + handlerClassInstance: spyHandler2, + }, + ]); + const spyRegistryFind = spyOn( + SystemEventHandlerRegistry, + "findHandlerByReference", + ).and.returnValue({ + handlerRef: () => Promise.resolve(true), + handlerRecord: { + handlerRef: () => Promise.resolve(true), + handlerClassInstance: null, + handlerFilterExpression: null, + }, + active: true, + }); + const payload = {}; + + // When + SystemEventDispatcher.post(SE_NAVIGATE, payload, 1); + + // Then + tick(100); + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).toHaveBeenCalledWith( + payload, + SE_NAVIGATE, + ); + expect(spyHandler2.evaluate).not.toHaveBeenCalled(); + expect(spyRegistryFind).toHaveBeenCalledTimes(1); + })); + }); + + describe("|send|", () => { + it("should verify will send BLOCKING System Event to 2 Handlers", fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler1", ["evaluate"]); + const spyHandler2: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler2", ["evaluate"]); + spyHandler1.evaluate.and.returnValue(Promise.resolve(true)); + spyHandler2.evaluate.and.returnValue(Promise.resolve(true)); + + const spyRegistryPrepared = spyOn( + SystemEventHandlerRegistry, + "getPreparedArrayOfHandlers", + ).and.returnValue([ + { + handlerRef: spyHandler1.evaluate, + handlerClassInstance: spyHandler1, + }, + { + handlerRef: spyHandler2.evaluate, + handlerClassInstance: spyHandler2, + }, + ]); + const payload = {}; + + // When + SystemEventDispatcher.send(SE_NAVIGATE, payload).then(() => { + // No-op. + }); + + tick(100); + + // Then + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).toHaveBeenCalledWith( + payload, + SE_NAVIGATE, + ); + expect(spyHandler2.evaluate).toHaveBeenCalledWith( + payload, + SE_NAVIGATE, + ); + })); + + it("should verify will send BLOCKING System Event to 1 Handler and will skip the second one", fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler1", ["evaluate"]); + const spyHandler2: jasmine.SpyObj<{ + evaluate: SystemEventHandlerRef; + }> = jasmine.createSpyObj("handler2", ["evaluate"]); + spyHandler1.evaluate.and.returnValue(Promise.resolve(true)); + spyHandler2.evaluate.and.returnValue(Promise.resolve(true)); + + const spyRegistryPrepared = spyOn( + SystemEventHandlerRegistry, + "getPreparedArrayOfHandlers", + ).and.returnValue([ + { + handlerRef: spyHandler1.evaluate, + handlerClassInstance: spyHandler1, + }, + { + handlerRef: spyHandler2.evaluate, + handlerClassInstance: spyHandler2, + }, + ]); + const payload = {}; + + // When + SystemEventDispatcher.send(SE_NAVIGATE, payload, 1).then(() => { + // No-op. + }); + + tick(100); + + // Then + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).not.toHaveBeenCalled(); + expect(spyHandler2.evaluate).toHaveBeenCalledWith( + payload, + SE_NAVIGATE, + ); + })); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts index ce624cc15f..3821738c72 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts @@ -5,11 +5,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { SE_ALL_EVENTS, SystemEvent, SystemEventComparable } from '../event'; -import { SystemEventHandlerRegistry } from './registry'; -import { SystemEventHandlerRecord } from './models'; +import { SE_ALL_EVENTS, SystemEvent, SystemEventComparable } from "../event"; +import { SystemEventHandlerRegistry } from "./registry"; +import { SystemEventHandlerRecord } from "./models"; /** * ** System Event Dispatcher. @@ -42,101 +42,160 @@ import { SystemEventHandlerRecord } from './models'; * }); */ export class SystemEventDispatcher { - /** - * ** Method to post System Event. - * - * - NON-BLOCKING execution. - * - Execution is non-blocking, e.g. it is executed in queue using setTimeout of 0. - * - If third parameters is provided it will execute as many handlers as it is request. - * - e.g. 1 or 2 or N and will skip the others. - */ - static post(eventId: SystemEvent, payload: any, handlersToExecute: number = null) { - const preparedHandlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(eventId); - - let executedHandlers = 0; - - for (const handlerRecord of preparedHandlers) { - if (!SystemEventDispatcher.executeExpressionFilter(handlerRecord, eventId, payload)) { - return; - } - - executedHandlers++; - - const isHandlerActive = (e: SystemEvent) => - SystemEventHandlerRegistry.findHandlerByReference(e, handlerRecord.handlerRef, handlerRecord.handlerClassInstance).active; - - setTimeout(() => { - if (!isHandlerActive(eventId) && !isHandlerActive(SE_ALL_EVENTS)) { - return; - } - - handlerRecord.handlerRef.call(handlerRecord.handlerClassInstance, payload, eventId); - }, 0); - - if (CollectionsUtil.isNumber(handlersToExecute) && executedHandlers >= handlersToExecute) { - break; - } + /** + * ** Method to post System Event. + * + * - NON-BLOCKING execution. + * - Execution is non-blocking, e.g. it is executed in queue using setTimeout of 0. + * - If third parameters is provided it will execute as many handlers as it is request. + * - e.g. 1 or 2 or N and will skip the others. + */ + static post( + eventId: SystemEvent, + payload: any, + handlersToExecute: number = null, + ) { + const preparedHandlers = + SystemEventHandlerRegistry.getPreparedArrayOfHandlers(eventId); + + let executedHandlers = 0; + + for (const handlerRecord of preparedHandlers) { + if ( + !SystemEventDispatcher.executeExpressionFilter( + handlerRecord, + eventId, + payload, + ) + ) { + return; + } + + executedHandlers++; + + const isHandlerActive = (e: SystemEvent) => + SystemEventHandlerRegistry.findHandlerByReference( + e, + handlerRecord.handlerRef, + handlerRecord.handlerClassInstance, + ).active; + + setTimeout(() => { + if (!isHandlerActive(eventId) && !isHandlerActive(SE_ALL_EVENTS)) { + return; } - } - /** - * ** Method to send System Event. - * - * - BLOCKING execution. - * - Execution is blocking, e.g. it is achieved using Promises, and every handler must return Promise. - * - If third parameters is provided it will execute as many handlers as it is request. - * - e.g. 1 or 2 or N and will return the flow to the invoker. - */ - static send(eventId: SystemEvent, payload: any, handlersToExecute: number = null): Promise { - const preparedHandlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(eventId, true); - - return SystemEventDispatcher.executeSendCommand(preparedHandlers, eventId, payload, handlersToExecute); + handlerRecord.handlerRef.call( + handlerRecord.handlerClassInstance, + payload, + eventId, + ); + }, 0); + + if ( + CollectionsUtil.isNumber(handlersToExecute) && + executedHandlers >= handlersToExecute + ) { + break; + } } - - /** - * ** Execute send command and handle Promises from handlers. - */ - private static executeSendCommand( - handlers: SystemEventHandlerRecord[], - eventId: SystemEvent, - payload: any, - handlersToExecute: number = null, - executedHandlers = 0 - ): Promise { - if (!handlers.length) { - return Promise.resolve(true); - } - - if (CollectionsUtil.isNumber(handlersToExecute) && handlersToExecute === executedHandlers) { - return Promise.resolve(true); - } - - const handlerForExecution = handlers.pop(); - - if (!SystemEventDispatcher.executeExpressionFilter(handlerForExecution, eventId, payload)) { - return SystemEventDispatcher.executeSendCommand(handlers, eventId, payload, handlersToExecute, executedHandlers); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return - return handlerForExecution.handlerRef - .call(handlerForExecution.handlerClassInstance, payload, eventId) - .then(() => SystemEventDispatcher.executeSendCommand(handlers, eventId, payload, handlersToExecute, executedHandlers + 1)) - .catch(() => Promise.reject(false)); + } + + /** + * ** Method to send System Event. + * + * - BLOCKING execution. + * - Execution is blocking, e.g. it is achieved using Promises, and every handler must return Promise. + * - If third parameters is provided it will execute as many handlers as it is request. + * - e.g. 1 or 2 or N and will return the flow to the invoker. + */ + static send( + eventId: SystemEvent, + payload: any, + handlersToExecute: number = null, + ): Promise { + const preparedHandlers = + SystemEventHandlerRegistry.getPreparedArrayOfHandlers(eventId, true); + + return SystemEventDispatcher.executeSendCommand( + preparedHandlers, + eventId, + payload, + handlersToExecute, + ); + } + + /** + * ** Execute send command and handle Promises from handlers. + */ + private static executeSendCommand( + handlers: SystemEventHandlerRecord[], + eventId: SystemEvent, + payload: any, + handlersToExecute: number = null, + executedHandlers = 0, + ): Promise { + if (!handlers.length) { + return Promise.resolve(true); } - /** - * ** Execute Expression filter for Handler before execution. - */ - private static executeExpressionFilter(handlerRecord: SystemEventHandlerRecord, eventId: SystemEvent, payload: any): boolean { - const filterExpression = handlerRecord.handlerFilterExpression; + if ( + CollectionsUtil.isNumber(handlersToExecute) && + handlersToExecute === executedHandlers + ) { + return Promise.resolve(true); + } - if (CollectionsUtil.isNil(filterExpression)) { - return true; - } + const handlerForExecution = handlers.pop(); + + if ( + !SystemEventDispatcher.executeExpressionFilter( + handlerForExecution, + eventId, + payload, + ) + ) { + return SystemEventDispatcher.executeSendCommand( + handlers, + eventId, + payload, + handlersToExecute, + executedHandlers, + ); + } - return handlerRecord.handlerFilterExpression.evaluate( - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - SystemEventComparable.of({ eventId, payload }) - ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return + return handlerForExecution.handlerRef + .call(handlerForExecution.handlerClassInstance, payload, eventId) + .then(() => + SystemEventDispatcher.executeSendCommand( + handlers, + eventId, + payload, + handlersToExecute, + executedHandlers + 1, + ), + ) + .catch(() => Promise.reject(false)); + } + + /** + * ** Execute Expression filter for Handler before execution. + */ + private static executeExpressionFilter( + handlerRecord: SystemEventHandlerRecord, + eventId: SystemEvent, + payload: any, + ): boolean { + const filterExpression = handlerRecord.handlerFilterExpression; + + if (CollectionsUtil.isNil(filterExpression)) { + return true; } + + return handlerRecord.handlerFilterExpression.evaluate( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + SystemEventComparable.of({ eventId, payload }), + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts index 161438d97c..198befa56e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event.dispatcher'; +export * from "./event.dispatcher"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts index aa17398c5c..6b76198f53 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts @@ -5,39 +5,46 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Expression } from '../../../../common'; +import { Expression } from "../../../../common"; -import { SystemEvent } from '../../event'; +import { SystemEvent } from "../../event"; export type SystemEventMetadataRecord = { - handler: SystemEventHandlerRef; - events: SystemEvent | SystemEvent[]; - filterExpression: Expression; + handler: SystemEventHandlerRef; + events: SystemEvent | SystemEvent[]; + filterExpression: Expression; }; /** * ** Type for one Handler interface. */ -export type SystemEventHandlerRef = (payload: any, eventId?: SystemEvent) => Promise; +export type SystemEventHandlerRef = ( + payload: any, + eventId?: SystemEvent, +) => Promise; /** * ** Event Handler record in registry. */ export type SystemEventHandlerRecord = { - /** - * ** Reference to Handler instance. - */ - handlerRef: SystemEventHandlerRef; - - /** - * ** Reference to the object where Handler belongs, for context purpose. - */ - handlerClassInstance?: Record; - - /** - * ** Expression that filters if some Event should be dispatch to the Handler reference or not. - */ - handlerFilterExpression?: Expression; + /** + * ** Reference to Handler instance. + */ + handlerRef: SystemEventHandlerRef; + + /** + * ** Reference to the object where Handler belongs, for context purpose. + */ + handlerClassInstance?: Record; + + /** + * ** Expression that filters if some Event should be dispatch to the Handler reference or not. + */ + handlerFilterExpression?: Expression; +}; +export type SystemEventHandlerFindByRef = { + active: boolean; + handlerRecord: SystemEventHandlerRecord; + handlerRef: SystemEventHandlerRef; }; -export type SystemEventHandlerFindByRef = { active: boolean; handlerRecord: SystemEventHandlerRecord; handlerRef: SystemEventHandlerRef }; export type SystemEventMetadataRecords = SystemEventMetadataRecord[]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts index e0de035c33..d49e753b95 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event-dispatcher.model'; +export * from "./event-dispatcher.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts index 6c46823fd2..286b0fc0d0 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts @@ -3,203 +3,289 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SystemEventHandlerRegistry } from './event-handler.registry'; +import { SystemEventHandlerRegistry } from "./event-handler.registry"; class TestClazz {} -describe('SystemEventHandlerRegistry', () => { - describe('Statics::', () => { - describe('Getters::()', () => { - describe('|instance|', () => { - it('should verify will return always same Repository instance', () => { - // When - const r1 = SystemEventHandlerRegistry.instance; - const r2 = SystemEventHandlerRegistry.instance; - const r3 = SystemEventHandlerRegistry.instance; - - // Then - expect(r2).toBe(r1); - expect(r3).toBe(r2); - }); - }); +describe("SystemEventHandlerRegistry", () => { + describe("Statics::", () => { + describe("Getters::()", () => { + describe("|instance|", () => { + it("should verify will return always same Repository instance", () => { + // When + const r1 = SystemEventHandlerRegistry.instance; + const r2 = SystemEventHandlerRegistry.instance; + const r3 = SystemEventHandlerRegistry.instance; + + // Then + expect(r2).toBe(r1); + expect(r3).toBe(r2); + }); + }); + }); + + describe("Methods::()", () => { + describe("|register|", () => { + it("should verify will execute register and execute methods in Registry instance", () => { + // Given + // @ts-ignore + const spyRegister = spyOn( + SystemEventHandlerRegistry.instance, + "register", + ).and.callThrough(); + // @ts-ignore + const spyExecute = spyOn( + SystemEventHandlerRegistry.instance, + "execute", + ).and.callThrough(); + const handler = () => Promise.resolve(true); + const handlerClassInstance = {}; + + // When + SystemEventHandlerRegistry.register( + "SE_Send_Email", + handler, + handlerClassInstance, + ); + + // Then + // @ts-ignore + expect(spyRegister).toHaveBeenCalledWith( + "SE_Send_Email", + handler, + handlerClassInstance, + null, + ); + // @ts-ignore + expect(spyExecute).toHaveBeenCalledWith( + "SE_Send_Email", + handler, + jasmine.any(Function), + handlerClassInstance, + null, + ); + }); + + it("should verify will register handler in Registry", () => { + // Given + const event = "SE_Create_Job"; + const handlerRef = () => Promise.resolve(false); + const clazzInstance = new TestClazz(); + + // When + SystemEventHandlerRegistry.register(event, handlerRef, clazzInstance); + + // Then + // eslint-disable-next-line @typescript-eslint/dot-notation + const registry = SystemEventHandlerRegistry.instance["handlers"]; + const handlers = registry.get(event); + expect(Array.from(handlers.values())).toContain({ + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance, + }); + }); + }); + + describe("|unregister|", () => { + it("should verify will execute unregister and execute methods in Registry instance", () => { + // Given + // @ts-ignore + const spyUnregister = spyOn( + SystemEventHandlerRegistry.instance, + "unregister", + ).and.callThrough(); + // @ts-ignore + const spyExecute = spyOn( + SystemEventHandlerRegistry.instance, + "execute", + ).and.callThrough(); + const event = "SE_Delete_Job"; + const handler = () => Promise.resolve(true); + + // When + SystemEventHandlerRegistry.unregister(event, handler); + + // Then + // @ts-ignore + expect(spyUnregister).toHaveBeenCalledWith(event, handler); + // @ts-ignore + expect(spyExecute).toHaveBeenCalledWith( + event, + handler, + jasmine.any(Function), + ); + }); + + it("should verify will remove handler in Registry after it is added", () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const registry = SystemEventHandlerRegistry.instance["handlers"]; + const event = "SE_Update_Job"; + const handlerRef = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + // When 1 + SystemEventHandlerRegistry.register(event, handlerRef, clazzInstance); + + // Then 1 + const handlers = registry.get(event); + expect(Array.from(handlers.values())).toContain({ + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance, + }); + + // When 2 + SystemEventHandlerRegistry.unregister(event, handlerRef); + + // Then 2 + // eslint-disable-next-line @typescript-eslint/dot-notation + expect(Array.from(handlers.values())).not.toContain({ + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance, + }); + }); + }); + + describe("|findHandlerByReference|", () => { + it("should verify will find Handler by its reference", () => { + // Given + const event = "SE_Notify_Users"; + const handlerRef = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + // When + SystemEventHandlerRegistry.register(event, handlerRef, clazzInstance); + const record = SystemEventHandlerRegistry.findHandlerByReference( + event, + handlerRef, + clazzInstance, + ); + + // Then + expect(record).toEqual({ + active: true, + handlerRef, + handlerRecord: { + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance, + }, + }); + }); + }); + + describe("|getPreparedArrayOfHandlers|", () => { + it("should verify will return empty Array when there is no Handlers for Event", () => { + // Given + const event = "SE_Clean_Jobs"; + + // When + const handlers = + SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event); + + // Then + expect(handlers).toEqual([]); + }); + + it("should verify will return 3 handlers for given Event", () => { + // Given + const event = "SE_Log_Incident"; + const handlerRef1 = () => Promise.resolve(true); + const handlerRef2 = () => Promise.resolve(true); + const handlerRef3 = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + SystemEventHandlerRegistry.register( + event, + handlerRef1, + clazzInstance, + ); + SystemEventHandlerRegistry.register( + event, + handlerRef2, + clazzInstance, + ); + SystemEventHandlerRegistry.register( + event, + handlerRef3, + clazzInstance, + ); + + // When + const handlers = + SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event); + + // Then + expect(handlers).toEqual([ + { + handlerRef: handlerRef1, + handlerClassInstance: clazzInstance, + handlerFilterExpression: null, + }, + { + handlerRef: handlerRef2, + handlerClassInstance: clazzInstance, + handlerFilterExpression: null, + }, + { + handlerRef: handlerRef3, + handlerClassInstance: clazzInstance, + handlerFilterExpression: null, + }, + ]); }); - describe('Methods::()', () => { - describe('|register|', () => { - it('should verify will execute register and execute methods in Registry instance', () => { - // Given - // @ts-ignore - const spyRegister = spyOn(SystemEventHandlerRegistry.instance, 'register').and.callThrough(); - // @ts-ignore - const spyExecute = spyOn(SystemEventHandlerRegistry.instance, 'execute').and.callThrough(); - const handler = () => Promise.resolve(true); - const handlerClassInstance = {}; - - // When - SystemEventHandlerRegistry.register('SE_Send_Email', handler, handlerClassInstance); - - // Then - // @ts-ignore - expect(spyRegister).toHaveBeenCalledWith('SE_Send_Email', handler, handlerClassInstance, null); - // @ts-ignore - expect(spyExecute).toHaveBeenCalledWith('SE_Send_Email', handler, jasmine.any(Function), handlerClassInstance, null); - }); - - it('should verify will register handler in Registry', () => { - // Given - const event = 'SE_Create_Job'; - const handlerRef = () => Promise.resolve(false); - const clazzInstance = new TestClazz(); - - // When - SystemEventHandlerRegistry.register(event, handlerRef, clazzInstance); - - // Then - // eslint-disable-next-line @typescript-eslint/dot-notation - const registry = SystemEventHandlerRegistry.instance['handlers']; - const handlers = registry.get(event); - expect(Array.from(handlers.values())).toContain({ - handlerRef, - handlerFilterExpression: null, - handlerClassInstance: clazzInstance - }); - }); - }); - - describe('|unregister|', () => { - it('should verify will execute unregister and execute methods in Registry instance', () => { - // Given - // @ts-ignore - const spyUnregister = spyOn(SystemEventHandlerRegistry.instance, 'unregister').and.callThrough(); - // @ts-ignore - const spyExecute = spyOn(SystemEventHandlerRegistry.instance, 'execute').and.callThrough(); - const event = 'SE_Delete_Job'; - const handler = () => Promise.resolve(true); - - // When - SystemEventHandlerRegistry.unregister(event, handler); - - // Then - // @ts-ignore - expect(spyUnregister).toHaveBeenCalledWith(event, handler); - // @ts-ignore - expect(spyExecute).toHaveBeenCalledWith(event, handler, jasmine.any(Function)); - }); - - it('should verify will remove handler in Registry after it is added', () => { - // Given - // eslint-disable-next-line @typescript-eslint/dot-notation - const registry = SystemEventHandlerRegistry.instance['handlers']; - const event = 'SE_Update_Job'; - const handlerRef = () => Promise.resolve(true); - const clazzInstance = new TestClazz(); - - // When 1 - SystemEventHandlerRegistry.register(event, handlerRef, clazzInstance); - - // Then 1 - const handlers = registry.get(event); - expect(Array.from(handlers.values())).toContain({ - handlerRef, - handlerFilterExpression: null, - handlerClassInstance: clazzInstance - }); - - // When 2 - SystemEventHandlerRegistry.unregister(event, handlerRef); - - // Then 2 - // eslint-disable-next-line @typescript-eslint/dot-notation - expect(Array.from(handlers.values())).not.toContain({ - handlerRef, - handlerFilterExpression: null, - handlerClassInstance: clazzInstance - }); - }); - }); - - describe('|findHandlerByReference|', () => { - it('should verify will find Handler by its reference', () => { - // Given - const event = 'SE_Notify_Users'; - const handlerRef = () => Promise.resolve(true); - const clazzInstance = new TestClazz(); - - // When - SystemEventHandlerRegistry.register(event, handlerRef, clazzInstance); - const record = SystemEventHandlerRegistry.findHandlerByReference(event, handlerRef, clazzInstance); - - // Then - expect(record).toEqual({ - active: true, - handlerRef, - handlerRecord: { - handlerRef, - handlerFilterExpression: null, - handlerClassInstance: clazzInstance - } - }); - }); - }); - - describe('|getPreparedArrayOfHandlers|', () => { - it('should verify will return empty Array when there is no Handlers for Event', () => { - // Given - const event = 'SE_Clean_Jobs'; - - // When - const handlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event); - - // Then - expect(handlers).toEqual([]); - }); - - it('should verify will return 3 handlers for given Event', () => { - // Given - const event = 'SE_Log_Incident'; - const handlerRef1 = () => Promise.resolve(true); - const handlerRef2 = () => Promise.resolve(true); - const handlerRef3 = () => Promise.resolve(true); - const clazzInstance = new TestClazz(); - - SystemEventHandlerRegistry.register(event, handlerRef1, clazzInstance); - SystemEventHandlerRegistry.register(event, handlerRef2, clazzInstance); - SystemEventHandlerRegistry.register(event, handlerRef3, clazzInstance); - - // When - const handlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event); - - // Then - expect(handlers).toEqual([ - { handlerRef: handlerRef1, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, - { handlerRef: handlerRef2, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, - { handlerRef: handlerRef3, handlerClassInstance: clazzInstance, handlerFilterExpression: null } - ]); - }); - - it('should verify will return 3 handlers in reverse order for given Event', () => { - // Given - const event = 'SE_Clean_Session'; - const handlerRef11 = () => Promise.resolve(true); - const handlerRef22 = () => Promise.resolve(true); - const handlerRef33 = () => Promise.resolve(true); - const clazzInstance = new TestClazz(); - - SystemEventHandlerRegistry.register(event, handlerRef11, clazzInstance); - SystemEventHandlerRegistry.register(event, handlerRef22, clazzInstance); - SystemEventHandlerRegistry.register(event, handlerRef33, clazzInstance); - - // When - const handlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event, true); - - // Then - expect(handlers).toEqual([ - { handlerRef: handlerRef33, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, - { handlerRef: handlerRef22, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, - { handlerRef: handlerRef11, handlerClassInstance: clazzInstance, handlerFilterExpression: null } - ]); - }); - }); + it("should verify will return 3 handlers in reverse order for given Event", () => { + // Given + const event = "SE_Clean_Session"; + const handlerRef11 = () => Promise.resolve(true); + const handlerRef22 = () => Promise.resolve(true); + const handlerRef33 = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + SystemEventHandlerRegistry.register( + event, + handlerRef11, + clazzInstance, + ); + SystemEventHandlerRegistry.register( + event, + handlerRef22, + clazzInstance, + ); + SystemEventHandlerRegistry.register( + event, + handlerRef33, + clazzInstance, + ); + + // When + const handlers = + SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event, true); + + // Then + expect(handlers).toEqual([ + { + handlerRef: handlerRef33, + handlerClassInstance: clazzInstance, + handlerFilterExpression: null, + }, + { + handlerRef: handlerRef22, + handlerClassInstance: clazzInstance, + handlerFilterExpression: null, + }, + { + handlerRef: handlerRef11, + handlerClassInstance: clazzInstance, + handlerFilterExpression: null, + }, + ]); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts index 9bcf5e42ba..ff0544087f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts @@ -5,234 +5,297 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { Expression } from '../../../../common'; +import { Expression } from "../../../../common"; -import { SE_ALL_EVENTS, SystemEvent } from '../../event'; -import { SystemEventHandlerFindByRef, SystemEventHandlerRecord, SystemEventHandlerRef } from '../models'; +import { SE_ALL_EVENTS, SystemEvent } from "../../event"; +import { + SystemEventHandlerFindByRef, + SystemEventHandlerRecord, + SystemEventHandlerRef, +} from "../models"; /** * ** Registry for Events Handlers. */ export class SystemEventHandlerRegistry { - /** - * ** Returns Singleton instance. - */ - static get instance(): SystemEventHandlerRegistry { - if (CollectionsUtil.isNil(SystemEventHandlerRegistry._instance)) { - SystemEventHandlerRegistry._instance = new SystemEventHandlerRegistry(); - } - - return SystemEventHandlerRegistry._instance; + /** + * ** Returns Singleton instance. + */ + static get instance(): SystemEventHandlerRegistry { + if (CollectionsUtil.isNil(SystemEventHandlerRegistry._instance)) { + SystemEventHandlerRegistry._instance = new SystemEventHandlerRegistry(); } - private static _instance: SystemEventHandlerRegistry; - - private readonly handlers: Map> = new Map>(); - - /** - * ** Constructor. - * - * - Private constructor to make it Singleton. - */ - private constructor() { - // No-op. - } - - /** - * ** Register Handler for SystemEvent/s. - */ - static register( - knownEvents: SystemEvent | SystemEvent[], - handlerRef: SystemEventHandlerRef, - handlerClassInstance: T, - handlerFilterExpression: Expression = null - ): boolean { - return SystemEventHandlerRegistry.instance.register(knownEvents, handlerRef, handlerClassInstance, handlerFilterExpression); - } - - /** - * ** Unregister Handler from registry. - * - * - It should be done in ngOnDestroy() method in Services/Components to avoid potential memory leaks. - */ - static unregister(knownEvents: SystemEvent | SystemEvent[], handlerRef: SystemEventHandlerRef): boolean { - return SystemEventHandlerRegistry.instance.unregister(knownEvents, handlerRef); + return SystemEventHandlerRegistry._instance; + } + + private static _instance: SystemEventHandlerRegistry; + + private readonly handlers: Map> = + new Map>(); + + /** + * ** Constructor. + * + * - Private constructor to make it Singleton. + */ + private constructor() { + // No-op. + } + + /** + * ** Register Handler for SystemEvent/s. + */ + static register( + knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + handlerClassInstance: T, + handlerFilterExpression: Expression = null, + ): boolean { + return SystemEventHandlerRegistry.instance.register( + knownEvents, + handlerRef, + handlerClassInstance, + handlerFilterExpression, + ); + } + + /** + * ** Unregister Handler from registry. + * + * - It should be done in ngOnDestroy() method in Services/Components to avoid potential memory leaks. + */ + static unregister( + knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + ): boolean { + return SystemEventHandlerRegistry.instance.unregister( + knownEvents, + handlerRef, + ); + } + + /** + * ** Find Handler by reference looking through the Registry. + */ + static findHandlerByReference( + eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + handlerClassInstance?: any, + ): SystemEventHandlerFindByRef { + return SystemEventHandlerRegistry.instance.findHandlerByReference( + eventId, + handlerRef, + handlerClassInstance, + ); + } + + /** + * ** Prepare array of handlers for execution of post or send. + */ + static getPreparedArrayOfHandlers( + eventId: SystemEvent, + reversed = false, + ): SystemEventHandlerRecord[] { + return SystemEventHandlerRegistry.instance.getPreparedArrayOfHandlers( + eventId, + reversed, + ); + } + + private register( + knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + handlerClassInstance: T, + handlerFilterExpression: Expression = null, + ): boolean { + return this.execute( + knownEvents, + handlerRef, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.executeRegister.bind(this), + handlerClassInstance, + handlerFilterExpression, + ); + } + + private unregister( + knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + ): boolean { + return this.execute( + knownEvents, + handlerRef, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.executeUnregister.bind(this), + ); + } + + private findHandlerByReference( + eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + handlerClassInstance?: any, + ): SystemEventHandlerFindByRef { + const handlers = this.handlers.has(eventId) + ? this.handlers.get(eventId) + : new Set(); + + const handlerRecord = Array.from(handlers.values()).find((r) => { + let isEqual = r.handlerRef === handlerRef; + + if (handlerClassInstance) { + isEqual = isEqual && r.handlerClassInstance === handlerClassInstance; + } + + return isEqual; + }); + + return { + active: CollectionsUtil.isDefined(handlerRecord), + handlerRecord, + handlerRef: CollectionsUtil.isDefined(handlerRecord) + ? handlerRecord.handlerRef + : undefined, + }; + } + + private getPreparedArrayOfHandlers( + eventId: SystemEvent, + reversed = false, + ): SystemEventHandlerRecord[] { + const specialHandlers: SystemEventHandlerRecord[] = + this.getSpecialHandlers(); + + if (!this.handlers.has(eventId)) { + return specialHandlers; } - /** - * ** Find Handler by reference looking through the Registry. - */ - static findHandlerByReference( - eventId: SystemEvent, - handlerRef: SystemEventHandlerRef, - handlerClassInstance?: any - ): SystemEventHandlerFindByRef { - return SystemEventHandlerRegistry.instance.findHandlerByReference(eventId, handlerRef, handlerClassInstance); + return reversed + ? Array.from(this.handlers.get(eventId).values()) + .concat(specialHandlers) + .reverse() + : Array.from(this.handlers.get(eventId).values()).concat(specialHandlers); + } + + /** + * ** Generic abstraction for Register and Unregister handlers. + */ + private execute( + knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + executeMethodRef: (...arg: any[]) => any, + handlerClassInstance?: any, + handlerFilterExpression?: Expression, + ): boolean { + const preparedData = this.prepareEventNames(knownEvents); + + if (!preparedData.status || !CollectionsUtil.isFunction(handlerRef)) { + return false; } - /** - * ** Prepare array of handlers for execution of post or send. - */ - static getPreparedArrayOfHandlers(eventId: SystemEvent, reversed = false): SystemEventHandlerRecord[] { - return SystemEventHandlerRegistry.instance.getPreparedArrayOfHandlers(eventId, reversed); - } - - private register( - knownEvents: SystemEvent | SystemEvent[], - handlerRef: SystemEventHandlerRef, - handlerClassInstance: T, - handlerFilterExpression: Expression = null - ): boolean { - return this.execute( - knownEvents, - handlerRef, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.executeRegister.bind(this), - handlerClassInstance, - handlerFilterExpression - ); - } - - private unregister(knownEvents: SystemEvent | SystemEvent[], handlerRef: SystemEventHandlerRef): boolean { - return this.execute( - knownEvents, - handlerRef, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.executeUnregister.bind(this) + try { + preparedData.eventNames.forEach((eventId) => { + executeMethodRef( + eventId, + handlerRef, + handlerClassInstance, + handlerFilterExpression, ); - } + }); - private findHandlerByReference( - eventId: SystemEvent, - handlerRef: SystemEventHandlerRef, - handlerClassInstance?: any - ): SystemEventHandlerFindByRef { - const handlers = this.handlers.has(eventId) ? this.handlers.get(eventId) : new Set(); - - const handlerRecord = Array.from(handlers.values()).find((r) => { - let isEqual = r.handlerRef === handlerRef; - - if (handlerClassInstance) { - isEqual = isEqual && r.handlerClassInstance === handlerClassInstance; - } - - return isEqual; - }); - - return { - active: CollectionsUtil.isDefined(handlerRecord), - handlerRecord, - handlerRef: CollectionsUtil.isDefined(handlerRecord) ? handlerRecord.handlerRef : undefined - }; + return true; + } catch (_e) { + return false; } - - private getPreparedArrayOfHandlers(eventId: SystemEvent, reversed = false): SystemEventHandlerRecord[] { - const specialHandlers: SystemEventHandlerRecord[] = this.getSpecialHandlers(); - - if (!this.handlers.has(eventId)) { - return specialHandlers; - } - - return reversed - ? Array.from(this.handlers.get(eventId).values()).concat(specialHandlers).reverse() - : Array.from(this.handlers.get(eventId).values()).concat(specialHandlers); + } + + /** + * ** Evaluate Handler register. + */ + private executeRegister( + eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + handlerClassInstance: any, + handlerFilterExpression: Expression, + ): boolean { + if (!this.handlers.has(eventId)) { + this.handlers.set(eventId, new Set()); } - /** - * ** Generic abstraction for Register and Unregister handlers. - */ - private execute( - knownEvents: SystemEvent | SystemEvent[], - handlerRef: SystemEventHandlerRef, - executeMethodRef: (...arg: any[]) => any, - handlerClassInstance?: any, - handlerFilterExpression?: Expression - ): boolean { - const preparedData = this.prepareEventNames(knownEvents); - - if (!preparedData.status || !CollectionsUtil.isFunction(handlerRef)) { - return false; - } - - try { - preparedData.eventNames.forEach((eventId) => { - executeMethodRef(eventId, handlerRef, handlerClassInstance, handlerFilterExpression); - }); - - return true; - } catch (_e) { - return false; - } + if ( + this.findHandlerByReference(eventId, handlerRef, handlerClassInstance) + .active + ) { + return false; } - /** - * ** Evaluate Handler register. - */ - private executeRegister( - eventId: SystemEvent, - handlerRef: SystemEventHandlerRef, - handlerClassInstance: any, - handlerFilterExpression: Expression - ): boolean { - if (!this.handlers.has(eventId)) { - this.handlers.set(eventId, new Set()); - } - - if (this.findHandlerByReference(eventId, handlerRef, handlerClassInstance).active) { - return false; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.handlers.get(eventId).add({ handlerRef, handlerClassInstance, handlerFilterExpression }); - - return true; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.handlers + .get(eventId) + .add({ handlerRef, handlerClassInstance, handlerFilterExpression }); + + return true; + } + + /** + * ** Evaluate Handler unregister. + */ + private executeUnregister( + eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + ): boolean { + if (!this.handlers.has(eventId)) { + return false; } - /** - * ** Evaluate Handler unregister. - */ - private executeUnregister(eventId: SystemEvent, handlerRef: SystemEventHandlerRef): boolean { - if (!this.handlers.has(eventId)) { - return false; - } - - const findHandlerResponse = this.findHandlerByReference(eventId, handlerRef); - - if (!findHandlerResponse.active) { - return false; - } + const findHandlerResponse = this.findHandlerByReference( + eventId, + handlerRef, + ); - this.handlers.get(eventId).delete(findHandlerResponse.handlerRecord); - - return true; + if (!findHandlerResponse.active) { + return false; } - /** - * ** Prepares Event names in Array of strings format. - */ - private prepareEventNames(knownEvents: SystemEvent | SystemEvent[]): { status: boolean; eventNames?: SystemEvent[] } { - if (!CollectionsUtil.isString(knownEvents) && !CollectionsUtil.isArray(knownEvents)) { - return { - status: false - }; - } - - return { - status: true, - eventNames: CollectionsUtil.isString(knownEvents) ? [knownEvents] : knownEvents - }; + this.handlers.get(eventId).delete(findHandlerResponse.handlerRecord); + + return true; + } + + /** + * ** Prepares Event names in Array of strings format. + */ + private prepareEventNames(knownEvents: SystemEvent | SystemEvent[]): { + status: boolean; + eventNames?: SystemEvent[]; + } { + if ( + !CollectionsUtil.isString(knownEvents) && + !CollectionsUtil.isArray(knownEvents) + ) { + return { + status: false, + }; } - /** - * ** Returns special Handlers. - */ - private getSpecialHandlers(): SystemEventHandlerRecord[] { - if (!this.handlers.has(SE_ALL_EVENTS)) { - return []; - } - - return Array.from(this.handlers.get(SE_ALL_EVENTS).values()); + return { + status: true, + eventNames: CollectionsUtil.isString(knownEvents) + ? [knownEvents] + : knownEvents, + }; + } + + /** + * ** Returns special Handlers. + */ + private getSpecialHandlers(): SystemEventHandlerRecord[] { + if (!this.handlers.has(SE_ALL_EVENTS)) { + return []; } + + return Array.from(this.handlers.get(SE_ALL_EVENTS).values()); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts index 0c4efea221..93ad29f976 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event-handler.registry'; +export * from "./event-handler.registry"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts index 146abb0e69..624824d26f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './models'; +export * from "./models"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts index 1a6f4474fc..e4e752f590 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts @@ -3,143 +3,148 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, ComparableImpl, Equal, Predicate } from '../../../../common'; - -import { SystemEventFilterExpression } from './event-filter.expression'; - -describe('SystemEventFilterExpression', () => { - let v1: any; - let v2: any; - let v3: any; - let v4: any; - - let c1: Comparable; - let c2: Comparable; - let c3: Comparable; - let c4: Comparable; - - let p1: Predicate; - let p2: Predicate; - let p3: Predicate; - let p4: Predicate; - - beforeEach(() => { - v1 = 'Taurus'; - v2 = 'Taurus'; - v3 = 'VDK'; - v4 = 'Taurus'; - - c1 = ComparableImpl.of(v1); - c2 = ComparableImpl.of(v2); - c3 = ComparableImpl.of(v3); - c4 = ComparableImpl.of(v4); - - p1 = Equal.of(c1); - p2 = Equal.of(c2); - p3 = Equal.of(c3); - p4 = Equal.of(c4); +import { + Comparable, + ComparableImpl, + Equal, + Predicate, +} from "../../../../common"; + +import { SystemEventFilterExpression } from "./event-filter.expression"; + +describe("SystemEventFilterExpression", () => { + let v1: any; + let v2: any; + let v3: any; + let v4: any; + + let c1: Comparable; + let c2: Comparable; + let c3: Comparable; + let c4: Comparable; + + let p1: Predicate; + let p2: Predicate; + let p3: Predicate; + let p4: Predicate; + + beforeEach(() => { + v1 = "Taurus"; + v2 = "Taurus"; + v3 = "VDK"; + v4 = "Taurus"; + + c1 = ComparableImpl.of(v1); + c2 = ComparableImpl.of(v2); + c3 = ComparableImpl.of(v3); + c4 = ComparableImpl.of(v4); + + p1 = Equal.of(c1); + p2 = Equal.of(c2); + p3 = Equal.of(c3); + p4 = Equal.of(c4); + }); + + it("should verify instance is created", () => { + // When + const r = new SystemEventFilterExpression(p1); + + // Then + expect(r).toBeDefined(); + }); + + it("should verify Predicates are correctly assigned", () => { + // When + const r = new SystemEventFilterExpression(p1, p2, p3); + + // Then + expect(r.predicates).toEqual([p1, p2, p3]); + }); + + it("should verify is no Predicates given will create empty Expression", () => { + // When + const r1 = SystemEventFilterExpression.of(); + const r2 = new SystemEventFilterExpression(); + + // Then + expect(r1).toBeDefined(); + expect(r1.predicates).toEqual([]); + expect(r2).toBeDefined(); + expect(r2.predicates).toEqual([]); + }); + + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // When + const r = SystemEventFilterExpression.of(p1, p2, p3, p4); + + // Then + expect(r).toBeDefined(); + expect(r).toBeInstanceOf(SystemEventFilterExpression); + expect(r.predicates).toEqual([p1, p2, p3, p4]); + }); + }); }); + }); - it('should verify instance is created', () => { - // When - const r = new SystemEventFilterExpression(p1); + describe("Methods::()", () => { + describe("|addPredicate|", () => { + it("should verify will add new Predicates to existing one", () => { + // Given + const e = SystemEventFilterExpression.of(p1); - // Then - expect(r).toBeDefined(); - }); + // Then 1 + expect(e.predicates).toEqual([p1]); - it('should verify Predicates are correctly assigned', () => { // When - const r = new SystemEventFilterExpression(p1, p2, p3); + e.addPredicate(p3, p4); + e.addPredicate(p2); - // Then - expect(r.predicates).toEqual([p1, p2, p3]); + // Then 2 + expect(e.predicates).toEqual([p1, p3, p4, p2]); + }); }); - it('should verify is no Predicates given will create empty Expression', () => { + describe("|hasPredicates|", () => { + it("should verify will return true if there is Predicates in Expression, otherwise false", () => { + // Given + const e1 = SystemEventFilterExpression.of(); + const e2 = SystemEventFilterExpression.of(p1, p2); + // When - const r1 = SystemEventFilterExpression.of(); - const r2 = new SystemEventFilterExpression(); + const r1 = e1.hasPredicates(); + const r2 = e2.hasPredicates(); // Then - expect(r1).toBeDefined(); - expect(r1.predicates).toEqual([]); - expect(r2).toBeDefined(); - expect(r2.predicates).toEqual([]); + expect(r1).toBeFalse(); + expect(r2).toBeTrue(); + }); }); - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // When - const r = SystemEventFilterExpression.of(p1, p2, p3, p4); - - // Then - expect(r).toBeDefined(); - expect(r).toBeInstanceOf(SystemEventFilterExpression); - expect(r.predicates).toEqual([p1, p2, p3, p4]); - }); - }); - }); - }); - - describe('Methods::()', () => { - describe('|addPredicate|', () => { - it('should verify will add new Predicates to existing one', () => { - // Given - const e = SystemEventFilterExpression.of(p1); + describe("|evaluate|", () => { + it("should verify will return true only when all predicates return true", () => { + // Given + const e = SystemEventFilterExpression.of(p1, p2); - // Then 1 - expect(e.predicates).toEqual([p1]); - - // When - e.addPredicate(p3, p4); - e.addPredicate(p2); - - // Then 2 - expect(e.predicates).toEqual([p1, p3, p4, p2]); - }); - }); - - describe('|hasPredicates|', () => { - it('should verify will return true if there is Predicates in Expression, otherwise false', () => { - // Given - const e1 = SystemEventFilterExpression.of(); - const e2 = SystemEventFilterExpression.of(p1, p2); - - // When - const r1 = e1.hasPredicates(); - const r2 = e2.hasPredicates(); - - // Then - expect(r1).toBeFalse(); - expect(r2).toBeTrue(); - }); - }); - - describe('|evaluate|', () => { - it('should verify will return true only when all predicates return true', () => { - // Given - const e = SystemEventFilterExpression.of(p1, p2); - - // When - const r = e.evaluate(c4); + // When + const r = e.evaluate(c4); - // Then - expect(r).toBeTrue(); - }); + // Then + expect(r).toBeTrue(); + }); - it('should verify will return false if one of the Predicates return false', () => { - // Given - const e = SystemEventFilterExpression.of(p1, p2, p3); + it("should verify will return false if one of the Predicates return false", () => { + // Given + const e = SystemEventFilterExpression.of(p1, p2, p3); - // When - const r = e.evaluate(c4); + // When + const r = e.evaluate(c4); - // Then - expect(r).toBeFalse(); - }); - }); + // Then + expect(r).toBeFalse(); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts index b01986a19c..2fb742fcd8 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts @@ -3,49 +3,49 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, Expression, Predicate } from '../../../../common'; +import { Comparable, Expression, Predicate } from "../../../../common"; /** * ** System Event Filter Expression that evaluates if some Handler should be executed or no. */ export class SystemEventFilterExpression implements Expression { - /** - * @inheritDoc - */ - public readonly predicates: Predicate[]; + /** + * @inheritDoc + */ + public readonly predicates: Predicate[]; - /** - * ** Constructor. - */ - constructor(...predicates: Predicate[]) { - this.predicates = predicates ?? []; - } + /** + * ** Constructor. + */ + constructor(...predicates: Predicate[]) { + this.predicates = predicates ?? []; + } - /** - * ** Factory method. - */ - static of(...predicates: Predicate[]): SystemEventFilterExpression { - return new SystemEventFilterExpression(...predicates); - } + /** + * ** Factory method. + */ + static of(...predicates: Predicate[]): SystemEventFilterExpression { + return new SystemEventFilterExpression(...predicates); + } - /** - * ** Add Predicates to Expression. - */ - addPredicate(...predicates: Predicate[]): void { - this.predicates.push(...predicates); - } + /** + * ** Add Predicates to Expression. + */ + addPredicate(...predicates: Predicate[]): void { + this.predicates.push(...predicates); + } - /** - * ** Returns value that reflects if there is any Predicate inside the Expression (SystemEventFilter). - */ - hasPredicates(): boolean { - return this.predicates.length > 0; - } + /** + * ** Returns value that reflects if there is any Predicate inside the Expression (SystemEventFilter). + */ + hasPredicates(): boolean { + return this.predicates.length > 0; + } - /** - * @inheritDoc - */ - evaluate(comparable?: Comparable): boolean { - return this.predicates.every((p) => p.evaluate(comparable)); - } + /** + * @inheritDoc + */ + evaluate(comparable?: Comparable): boolean { + return this.predicates.every((p) => p.evaluate(comparable)); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts index 48bef94c57..372fb6d8b1 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NavigationExtras } from '@angular/router'; +import { NavigationExtras } from "@angular/router"; /** * ** System Event ID for navigation trigger. @@ -13,7 +13,7 @@ import { NavigationExtras } from '@angular/router'; * * - Payload {@link SystemEventNavigatePayload} */ -export const SE_NAVIGATE = 'SE_Navigate'; +export const SE_NAVIGATE = "SE_Navigate"; /** * ** System Event ID for location change through {@link @angular/common/Location}. @@ -24,7 +24,7 @@ export const SE_NAVIGATE = 'SE_Navigate'; * * - Payload {@link SystemEventLocationChangePayload} */ -export const SE_LOCATION_CHANGE = 'SE_Location_Change'; +export const SE_LOCATION_CHANGE = "SE_Location_Change"; /** * ** System Event that could be consumed by Handlers. @@ -36,7 +36,7 @@ export const SE_LOCATION_CHANGE = 'SE_Location_Change'; * * - Payload {any} */ -export const SE_ALL_EVENTS = '*'; +export const SE_ALL_EVENTS = "*"; // events payload types @@ -44,38 +44,38 @@ export const SE_ALL_EVENTS = '*'; * ** Payload send whenever {@link SE_NAVIGATE} event is fired. */ export interface SystemEventNavigatePayload { - url: string | string[]; - extras?: NavigationExtras; + url: string | string[]; + extras?: NavigationExtras; } /** * ** Payload post whenever {@link SE_LOCATION_CHANGE} event is fired. */ export interface SystemEventLocationChangePayload { - /** - * ** Url in string format. - * - * - e.g. '/pathname/path-param_1/path-param_2?query-param-1=value_1&query-param-2=value_2' - */ - url: string; - /** - * ** Dynamic path params in key-value map format. - */ - params: { [key: string]: string }; - /** - * ** Dynamic path params serialized in string format. - * - * - e.g. '/pathname/path-param_1/path-param_2' - */ - paramsSerialized: string; - /** - * ** Dynamic query params in key-value map format. - */ - queryParams: { [key: string]: string }; - /** - * ** Dynamic query params serialized in string format. - * - * - e.g. 'query-param-1=value_1&query-param-2=value_2' - */ - queryParamsSerialized: string; + /** + * ** Url in string format. + * + * - e.g. '/pathname/path-param_1/path-param_2?query-param-1=value_1&query-param-2=value_2' + */ + url: string; + /** + * ** Dynamic path params in key-value map format. + */ + params: { [key: string]: string }; + /** + * ** Dynamic path params serialized in string format. + * + * - e.g. '/pathname/path-param_1/path-param_2' + */ + paramsSerialized: string; + /** + * ** Dynamic query params in key-value map format. + */ + queryParams: { [key: string]: string }; + /** + * ** Dynamic query params serialized in string format. + * + * - e.g. 'query-param-1=value_1&query-param-2=value_2' + */ + queryParamsSerialized: string; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts index 64b1ad9887..ad60a6504c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts @@ -3,162 +3,162 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable } from '../../../../common/interfaces'; +import { Comparable } from "../../../../common/interfaces"; -import { SystemEventComparable } from './event.comparable'; +import { SystemEventComparable } from "./event.comparable"; class ComparableStub implements Comparable { - public readonly value: any; + public readonly value: any; - constructor(value: any) { - this.value = value; - } + constructor(value: any) { + this.value = value; + } - compare(_comparable: Comparable): number { - return 0; - } + compare(_comparable: Comparable): number { + return 0; + } - equal(_comparable: Comparable): boolean { - return true; - } + equal(_comparable: Comparable): boolean { + return true; + } - like(_comparable: Comparable): boolean { - return true; - } + like(_comparable: Comparable): boolean { + return true; + } - notEqual(_comparable: Comparable): boolean { - return false; - } + notEqual(_comparable: Comparable): boolean { + return false; + } - isNil(): boolean { - return false; - } + isNil(): boolean { + return false; + } - notNil(): boolean { - return true; - } + notNil(): boolean { + return true; + } - greaterThan(_comparable: Comparable): boolean { - return false; - } + greaterThan(_comparable: Comparable): boolean { + return false; + } - greaterThanInclusive(_comparable: Comparable): boolean { - return false; - } + greaterThanInclusive(_comparable: Comparable): boolean { + return false; + } - lessThan(_comparable: Comparable): boolean { - return false; - } + lessThan(_comparable: Comparable): boolean { + return false; + } - lessThanInclusive(_comparable: Comparable): boolean { - return false; - } + lessThanInclusive(_comparable: Comparable): boolean { + return false; + } } -describe('SystemEventComparable', () => { - it('should verify instance is created', () => { - // Given - const eventId = 'VDK'; - const payload = {}; +describe("SystemEventComparable", () => { + it("should verify instance is created", () => { + // Given + const eventId = "VDK"; + const payload = {}; + + // When + const instance = new SystemEventComparable({ eventId, payload }); + + // Then + expect(instance).toBeDefined(); + }); + + it("should verify value is correctly assigned", () => { + // Given + const eventId = "VDK"; + const payload = {}; + const value = { eventId, payload }; + + // When + const instance = new SystemEventComparable(value); + + // Then + expect(instance.value).toBe(value); + }); + + describe("Statics::", () => { + describe("Methods::()", () => { + describe("|of|", () => { + it("should verify factory method will create instance", () => { + // Given + const eventId = "VDK"; + const payload = {}; + const value = { eventId, payload }; + + // When + const instance = SystemEventComparable.of(value); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(SystemEventComparable); + }); + }); + }); + }); + + describe("Methods::()", () => { + let v1: { eventId: string; payload: unknown }; + let v2: { eventId: string; payload: unknown }; + let v3: { eventId: string; payload: unknown }; + let v4: { eventId: string; payload: unknown }; + + let c1: SystemEventComparable; + let c2: SystemEventComparable; + let c3: SystemEventComparable; + let c4: SystemEventComparable; + + beforeEach(() => { + v1 = { eventId: "Taurus", payload: { t: 10 } }; + v2 = { eventId: "VDK", payload: { t: 100 } }; + v3 = { eventId: "Test", payload: { t: 30 } }; + v4 = { eventId: "Taurus", payload: { t: 10 } }; + + c1 = SystemEventComparable.of(v1); + c2 = SystemEventComparable.of(v2); + c3 = SystemEventComparable.of(v3); + c4 = SystemEventComparable.of(v4); + }); + describe("|compare|", () => { + it("should verify will return 0 for equal", () => { // When - const instance = new SystemEventComparable({ eventId, payload }); + const comparison = c1.compare(c4); // Then - expect(instance).toBeDefined(); - }); - - it('should verify value is correctly assigned', () => { - // Given - const eventId = 'VDK'; - const payload = {}; - const value = { eventId, payload }; + expect(comparison).toEqual(0); + }); + it("should verify will return -1 for greaterThan", () => { // When - const instance = new SystemEventComparable(value); + const comparison = c2.compare(c1); // Then - expect(instance.value).toBe(value); - }); - - describe('Statics::', () => { - describe('Methods::()', () => { - describe('|of|', () => { - it('should verify factory method will create instance', () => { - // Given - const eventId = 'VDK'; - const payload = {}; - const value = { eventId, payload }; - - // When - const instance = SystemEventComparable.of(value); - - // Then - expect(instance).toBeDefined(); - expect(instance).toBeInstanceOf(SystemEventComparable); - }); - }); - }); - }); + expect(comparison).toEqual(-1); + }); - describe('Methods::()', () => { - let v1: { eventId: string; payload: unknown }; - let v2: { eventId: string; payload: unknown }; - let v3: { eventId: string; payload: unknown }; - let v4: { eventId: string; payload: unknown }; - - let c1: SystemEventComparable; - let c2: SystemEventComparable; - let c3: SystemEventComparable; - let c4: SystemEventComparable; - - beforeEach(() => { - v1 = { eventId: 'Taurus', payload: { t: 10 } }; - v2 = { eventId: 'VDK', payload: { t: 100 } }; - v3 = { eventId: 'Test', payload: { t: 30 } }; - v4 = { eventId: 'Taurus', payload: { t: 10 } }; - - c1 = SystemEventComparable.of(v1); - c2 = SystemEventComparable.of(v2); - c3 = SystemEventComparable.of(v3); - c4 = SystemEventComparable.of(v4); - }); - - describe('|compare|', () => { - it('should verify will return 0 for equal', () => { - // When - const comparison = c1.compare(c4); - - // Then - expect(comparison).toEqual(0); - }); - - it('should verify will return -1 for greaterThan', () => { - // When - const comparison = c2.compare(c1); - - // Then - expect(comparison).toEqual(-1); - }); - - it('should verify will return -1 for lessThan', () => { - // When - const comparison = c2.compare(c3); + it("should verify will return -1 for lessThan", () => { + // When + const comparison = c2.compare(c3); - // Then - expect(comparison).toEqual(-1); - }); + // Then + expect(comparison).toEqual(-1); + }); - it('should verify will return -1 given comparable is not instance of the current Constructor', () => { - // Given - const c10 = new ComparableStub(v2); + it("should verify will return -1 given comparable is not instance of the current Constructor", () => { + // Given + const c10 = new ComparableStub(v2); - // When - const comparison = c1.compare(c10); + // When + const comparison = c1.compare(c10); - // Then - expect(comparison).toEqual(-1); - }); - }); + // Then + expect(comparison).toEqual(-1); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts index 2c7d0151c6..61f686d176 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts @@ -3,40 +3,52 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Comparable, ComparableImpl } from '../../../../common'; +import { Comparable, ComparableImpl } from "../../../../common"; -import { CollectionsUtil } from '../../../../utils'; +import { CollectionsUtil } from "../../../../utils"; -import { SystemEvent } from './event-helper'; +import { SystemEvent } from "./event-helper"; /** * @inheritDoc */ -export class SystemEventComparable extends ComparableImpl<{ eventId: SystemEvent; payload: unknown }> { - /** - * ** Constructor. - */ - constructor(value: { eventId: string; payload: unknown }) { - super(value); - } +export class SystemEventComparable extends ComparableImpl<{ + eventId: SystemEvent; + payload: unknown; +}> { + /** + * ** Constructor. + */ + constructor(value: { eventId: string; payload: unknown }) { + super(value); + } - /** - * ** Factory method. - */ - static override of(value: { eventId: string; payload: unknown }): SystemEventComparable { - return new SystemEventComparable(value); - } + /** + * ** Factory method. + */ + static override of(value: { + eventId: string; + payload: unknown; + }): SystemEventComparable { + return new SystemEventComparable(value); + } - /** - * @inheritDoc - */ - override compare(comparable: Comparable): number { - if (comparable instanceof SystemEventComparable) { - const evaluateSecondStatement = () => (this.value.payload > comparable.value.payload ? 1 : -1); + /** + * @inheritDoc + */ + override compare(comparable: Comparable): number { + if (comparable instanceof SystemEventComparable) { + const evaluateSecondStatement = () => + this.value.payload > comparable.value.payload ? 1 : -1; - return CollectionsUtil.isEqual(this.value.payload, comparable.value.payload) ? 0 : evaluateSecondStatement(); - } else { - return -1; - } + return CollectionsUtil.isEqual( + this.value.payload, + comparable.value.payload, + ) + ? 0 + : evaluateSecondStatement(); + } else { + return -1; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts index 556617ded7..e6ca511450 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event.codes'; -export * from './event-helper'; -export * from './event-filter.expression'; -export * from './event.comparable'; +export * from "./event.codes"; +export * from "./event-helper"; +export * from "./event-filter.expression"; +export * from "./event.comparable"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts index fa83ea5f8a..3a2789d03c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './event'; -export * from './decorator'; -export * from './dispatcher'; +export * from "./event"; +export * from "./decorator"; +export * from "./dispatcher"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts index 946a892a4a..9437898f58 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts @@ -3,8 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { SystemEventHandler, SystemEventHandlerClass } from './decorator'; -export { SystemEventDispatcher } from './dispatcher'; -export { SystemEvent, SystemEventFilterExpression, SystemEventComparable } from './event'; +export { SystemEventHandler, SystemEventHandlerClass } from "./decorator"; +export { SystemEventDispatcher } from "./dispatcher"; +export { + SystemEvent, + SystemEventFilterExpression, + SystemEventComparable, +} from "./event"; // export all core system events -export * from './event/models/event.codes'; +export * from "./event/models/event.codes"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts index 28feb0c72d..ba7a6ddd61 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './url-state.manager'; +export * from "./url-state.manager"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts index 5870ac5363..ff93e538b6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { URLStateManager } from './index'; +export { URLStateManager } from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts index adba39e0b6..f1e019f525 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts @@ -5,14 +5,18 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Location } from '@angular/common'; +import { Location } from "@angular/common"; -import { SE_LOCATION_CHANGE, SE_NAVIGATE, SystemEventDispatcher } from '../system-events'; +import { + SE_LOCATION_CHANGE, + SE_NAVIGATE, + SystemEventDispatcher, +} from "../system-events"; export interface StateManagerParamValue { - key: string; - value: string; - position: number; + key: string; + value: string; + position: number; } /** @@ -22,281 +26,285 @@ export interface StateManagerParamValue { * - Provides ability to serialize current state as URL href. */ export class URLStateManager { - /** - * ** Store value if URL Params State mutated since previous navigation. - */ - public isParamsStateMutated = false; - - /** - * ** Store value if URL QueryParams State mutated since previous navigation. - */ - public isQueryParamsStateMutated = false; - - private readonly params: Map; - private readonly queryParams: Map; - private readonly locationHref: string; - - /** - * ** Constructor. - */ - constructor( - public baseURL: string, - public urlLocation: Location - ) { - this.params = new Map(); - this.queryParams = new Map(); - - this.locationHref = this.urlLocation.path(); + /** + * ** Store value if URL Params State mutated since previous navigation. + */ + public isParamsStateMutated = false; + + /** + * ** Store value if URL QueryParams State mutated since previous navigation. + */ + public isQueryParamsStateMutated = false; + + private readonly params: Map; + private readonly queryParams: Map; + private readonly locationHref: string; + + /** + * ** Constructor. + */ + constructor( + public baseURL: string, + public urlLocation: Location, + ) { + this.params = new Map(); + this.queryParams = new Map(); + + this.locationHref = this.urlLocation.path(); + } + + /** + * ** Returns current Browser URL href. + */ + get URL(): string { + if (this.baseURL) { + return `${this.baseURL}${this.getParamsToString()}${this.getQueryParamsToString()}`; } - /** - * ** Returns current Browser URL href. - */ - get URL(): string { - if (this.baseURL) { - return `${this.baseURL}${this.getParamsToString()}${this.getQueryParamsToString()}`; - } + return null; + } - return null; - } - - /** - * ** Replace current URL state to Browser URL. - */ - replaceToUrl(): void { - const browserCurrUrl = window.location.href; - - if (browserCurrUrl.endsWith(encodeURI(this.URL))) { - return; - } + /** + * ** Replace current URL state to Browser URL. + */ + replaceToUrl(): void { + const browserCurrUrl = window.location.href; - this.isParamsStateMutated = false; - - this.urlLocation.replaceState(this.URL); + if (browserCurrUrl.endsWith(encodeURI(this.URL))) { + return; } - /** - * ** Apply current URL state to Browser URL. - */ - locationToURL(): void { - const browserCurrUrl = window.location.href; - - if (browserCurrUrl.endsWith(encodeURI(this.URL))) { - return; - } - - this.isParamsStateMutated = false; + this.isParamsStateMutated = false; - this._notifyForLocationChange(); + this.urlLocation.replaceState(this.URL); + } - this.urlLocation.go(this.URL); - } + /** + * ** Apply current URL state to Browser URL. + */ + locationToURL(): void { + const browserCurrUrl = window.location.href; - /** - * ** Navigate through Angular Router with set URL state. - */ - navigateToUrl(): Promise { - const browserCurrUrl = window.location.href; - - if (browserCurrUrl.endsWith(encodeURI(this.URL))) { - return Promise.resolve(false); - } - - this.isQueryParamsStateMutated = false; - - return SystemEventDispatcher.send( - SE_NAVIGATE, - { - url: this.buildUrlWithParams(), - extras: { - queryParams: this.getQueryParamsAsMap() - } - }, - 1 - ); + if (browserCurrUrl.endsWith(encodeURI(this.URL))) { + return; } - /** - * ** Set query param to URL state. - */ - setQueryParam(key: string, value: string, position = 1): void { - this.isQueryParamsStateMutated = true; - - if (value) { - this.queryParams.set(key, { key, value, position }); - } else { - this.removeQueryParam(key); - } - } + this.isParamsStateMutated = false; - /** - * ** Returns query param value for given key. - */ - getQueryParam(key: string): string { - if (this.queryParams.has(key)) { - return this.queryParams.get(key).value; - } + this._notifyForLocationChange(); - return null; - } + this.urlLocation.go(this.URL); + } - /** - * ** Removes query param from URL state. - */ - removeQueryParam(key: string): void { - if (this.queryParams.has(key)) { - this.isQueryParamsStateMutated = true; + /** + * ** Navigate through Angular Router with set URL state. + */ + navigateToUrl(): Promise { + const browserCurrUrl = window.location.href; - this.queryParams.delete(key); - } + if (browserCurrUrl.endsWith(encodeURI(this.URL))) { + return Promise.resolve(false); } - /** - * ** Clear stored queryParams. - */ - clearQueryParams(): void { - this.queryParams.clear(); + this.isQueryParamsStateMutated = false; + + return SystemEventDispatcher.send( + SE_NAVIGATE, + { + url: this.buildUrlWithParams(), + extras: { + queryParams: this.getQueryParamsAsMap(), + }, + }, + 1, + ); + } + + /** + * ** Set query param to URL state. + */ + setQueryParam(key: string, value: string, position = 1): void { + this.isQueryParamsStateMutated = true; + + if (value) { + this.queryParams.set(key, { key, value, position }); + } else { + this.removeQueryParam(key); } - - /** - * ** Set param to URL state. - */ - setParam(key: string, value: string, position = 1): void { - this.isParamsStateMutated = true; - - if (value) { - this.params.set(key, { key, value, position }); - } else { - this.removeParam(key); - } + } + + /** + * ** Returns query param value for given key. + */ + getQueryParam(key: string): string { + if (this.queryParams.has(key)) { + return this.queryParams.get(key).value; } - /** - * ** Returns param value for given key. - */ - getParam(key: string): string { - if (this.params.has(key)) { - return this.params.get(key).value; - } - - return null; - } + return null; + } - /** - * ** Removes query param from URL state. - */ - removeParam(key: string): void { - if (this.params.has(key)) { - this.isParamsStateMutated = true; + /** + * ** Removes query param from URL state. + */ + removeQueryParam(key: string): void { + if (this.queryParams.has(key)) { + this.isQueryParamsStateMutated = true; - this.params.delete(key); - } + this.queryParams.delete(key); } - - /** - * ** Clear stored params. - */ - clearParams(): void { - this.params.clear(); + } + + /** + * ** Clear stored queryParams. + */ + clearQueryParams(): void { + this.queryParams.clear(); + } + + /** + * ** Set param to URL state. + */ + setParam(key: string, value: string, position = 1): void { + this.isParamsStateMutated = true; + + if (value) { + this.params.set(key, { key, value, position }); + } else { + this.removeParam(key); } - - /** - * ** Returns serialized params in string. - */ - getParamsToString(): string { - let paramString = ''; - - this.getSortedByPosition(this.params).forEach((p) => { - paramString += `/${p.value}`; - }); - - return paramString; + } + + /** + * ** Returns param value for given key. + */ + getParam(key: string): string { + if (this.params.has(key)) { + return this.params.get(key).value; } - /** - * ** Returns serialized queryParams in string. - */ - getQueryParamsToString(): string { - const sortedParams = this.getSortedByPosition(this.queryParams); - - let paramString = ''; - - if (sortedParams.length > 0) { - paramString = `?${sortedParams[0].key}=${sortedParams[0].value}`; + return null; + } - for (let i = 1; i < sortedParams.length; i++) { - const p = sortedParams[i]; - paramString += `&${p.key}=${p.value}`; - } - } + /** + * ** Removes query param from URL state. + */ + removeParam(key: string): void { + if (this.params.has(key)) { + this.isParamsStateMutated = true; - return paramString; + this.params.delete(key); } - - /** - * ** Returns query params in Map format. - */ - getQueryParamsAsMap(): { [key: string]: string } { - const sortedParams = this.getSortedByPosition(this.queryParams); - const paramsMap = {}; - - for (const paramsPair of sortedParams) { - paramsMap[paramsPair.key] = paramsPair.value; - } - - return paramsMap; + } + + /** + * ** Clear stored params. + */ + clearParams(): void { + this.params.clear(); + } + + /** + * ** Returns serialized params in string. + */ + getParamsToString(): string { + let paramString = ""; + + this.getSortedByPosition(this.params).forEach((p) => { + paramString += `/${p.value}`; + }); + + return paramString; + } + + /** + * ** Returns serialized queryParams in string. + */ + getQueryParamsToString(): string { + const sortedParams = this.getSortedByPosition(this.queryParams); + + let paramString = ""; + + if (sortedParams.length > 0) { + paramString = `?${sortedParams[0].key}=${sortedParams[0].value}`; + + for (let i = 1; i < sortedParams.length; i++) { + const p = sortedParams[i]; + paramString += `&${p.key}=${p.value}`; + } } - /** - * ** Returns params in Map format. - */ - getParamsAsMap(): { [key: string]: string } { - const sortedParams = this.getSortedByPosition(this.params); - const paramsMap = {}; + return paramString; + } - for (const paramsPair of sortedParams) { - paramsMap[paramsPair.key] = paramsPair.value; - } + /** + * ** Returns query params in Map format. + */ + getQueryParamsAsMap(): { [key: string]: string } { + const sortedParams = this.getSortedByPosition(this.queryParams); + const paramsMap = {}; - return paramsMap; + for (const paramsPair of sortedParams) { + paramsMap[paramsPair.key] = paramsPair.value; } - /** - * ** Build url from base and provided params. - */ - buildUrlWithParams(): string { - if (this.baseURL) { - return `${this.baseURL}${this.getParamsToString()}`; - } + return paramsMap; + } - return null; - } + /** + * ** Returns params in Map format. + */ + getParamsAsMap(): { [key: string]: string } { + const sortedParams = this.getSortedByPosition(this.params); + const paramsMap = {}; - /** - * ** Change Base url. - */ - changeBaseUrl(baseUrl: string): void { - this.baseURL = baseUrl; + for (const paramsPair of sortedParams) { + paramsMap[paramsPair.key] = paramsPair.value; } - private getSortedByPosition(values: Map): StateManagerParamValue[] { - return Array.from(values.entries()) - .sort((p1, p2) => p1[1].position - p2[1].position) - .map((e) => e[1]); - } + return paramsMap; + } - private _notifyForLocationChange(): void { - const paramsMap = this.getParamsAsMap(); - const paramsSerialized = this.buildUrlWithParams(); - const queryParamsMap = this.getQueryParamsAsMap(); - const queryParamsSerialized = this.getQueryParamsToString(); - - SystemEventDispatcher.post(SE_LOCATION_CHANGE, { - url: this.URL, - params: paramsMap ? paramsMap : {}, - paramsSerialized: paramsSerialized ? paramsSerialized : '', - queryParams: queryParamsMap ? queryParamsMap : {}, - queryParamsSerialized: queryParamsSerialized ? queryParamsSerialized.replace(/^\?/, '') : '' - }); + /** + * ** Build url from base and provided params. + */ + buildUrlWithParams(): string { + if (this.baseURL) { + return `${this.baseURL}${this.getParamsToString()}`; } + + return null; + } + + /** + * ** Change Base url. + */ + changeBaseUrl(baseUrl: string): void { + this.baseURL = baseUrl; + } + + private getSortedByPosition( + values: Map, + ): StateManagerParamValue[] { + return Array.from(values.entries()) + .sort((p1, p2) => p1[1].position - p2[1].position) + .map((e) => e[1]); + } + + private _notifyForLocationChange(): void { + const paramsMap = this.getParamsAsMap(); + const paramsSerialized = this.buildUrlWithParams(); + const queryParamsMap = this.getQueryParamsAsMap(); + const queryParamsSerialized = this.getQueryParamsToString(); + + SystemEventDispatcher.post(SE_LOCATION_CHANGE, { + url: this.URL, + params: paramsMap ? paramsMap : {}, + paramsSerialized: paramsSerialized ? paramsSerialized : "", + queryParams: queryParamsMap ? queryParamsMap : {}, + queryParamsSerialized: queryParamsSerialized + ? queryParamsSerialized.replace(/^\?/, "") + : "", + }); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/vdk-shared-core.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/vdk-shared-core.module.ts index 179a309f0e..11d1daced7 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/vdk-shared-core.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/vdk-shared-core.module.ts @@ -3,32 +3,41 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; +import { + ModuleWithProviders, + NgModule, + Optional, + SkipSelf, +} from "@angular/core"; -import { CookieService } from 'ngx-cookie-service'; +import { CookieService } from "ngx-cookie-service"; -import { NavigationService } from './navigation'; +import { NavigationService } from "./navigation"; @NgModule({}) export class VdkSharedCoreModule { - /** - * ** Constructor. - */ - constructor(@Optional() @SkipSelf() readonly vdkSharedCoreModule: VdkSharedCoreModule) { - if (vdkSharedCoreModule) { - throw new Error('VdkSharedCoreModule is already loaded. Import only once in root Module'); - } + /** + * ** Constructor. + */ + constructor( + @Optional() @SkipSelf() readonly vdkSharedCoreModule: VdkSharedCoreModule, + ) { + if (vdkSharedCoreModule) { + throw new Error( + "VdkSharedCoreModule is already loaded. Import only once in root Module", + ); } + } - /** - * ** Provides VDKSharedCore and all Services related to VDK Shared Core. - * - * - Should be executed once for entire project. - */ - static forRoot(): ModuleWithProviders { - return { - ngModule: VdkSharedCoreModule, - providers: [CookieService, NavigationService] - }; - } + /** + * ** Provides VDKSharedCore and all Services related to VDK Shared Core. + * + * - Should be executed once for entire project. + */ + static forRoot(): ModuleWithProviders { + return { + ngModule: VdkSharedCoreModule, + providers: [CookieService, NavigationService], + }; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/features.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/features.model.ts index 1bc961b4a1..e2919bcbc4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/features.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/features.model.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { WarningConfig } from '../warning'; -import { PlaceholderConfig } from '../placeholder'; +import { WarningConfig } from "../warning"; +import { PlaceholderConfig } from "../placeholder"; /** * ** Configuration that should be provided when Shared Features module is injected in the root of the application. diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/index.ts index 3cd712fc1f..652ebea0e3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './features.model'; +export * from "./features.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/public-api.ts index 27de297bf0..bdbc0b3e91 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_model/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index'; +export * from "./index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/features.token.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/features.token.ts index 70d58b19a3..56b61e9232 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/features.token.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/features.token.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { InjectionToken } from '@angular/core'; +import { InjectionToken } from "@angular/core"; -import { SharedFeaturesConfig } from '../_model'; +import { SharedFeaturesConfig } from "../_model"; /** * ** Injection token for Shared Features config. */ -export const SHARED_FEATURES_CONFIG_TOKEN = new InjectionToken('Shared Feature Config Token'); +export const SHARED_FEATURES_CONFIG_TOKEN = + new InjectionToken("Shared Feature Config Token"); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/index.ts index fda7a2852f..d31dcde5e2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/_token/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './features.token'; +export * from "./features.token"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.html index 1e70420eb0..40a708b945 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.html @@ -4,102 +4,122 @@ -->
    -
    -

    -
    - +
    +

    +
    +
    -

    +

    - + -
    - - - - - - -
    +
    + + + + + + +
    - + diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.scss index 7033be2abb..8c4f36042e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.scss @@ -4,77 +4,77 @@ */ :host { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .confirmation__header-container { - display: flex; - justify-content: space-between; - align-items: flex-start; - border-bottom: none; - padding: 0.5rem 0.5rem 0 0.5rem; + display: flex; + justify-content: space-between; + align-items: flex-start; + border-bottom: none; + padding: 0.5rem 0.5rem 0 0.5rem; - .confirmation__header-title-container { - flex: 1; + .confirmation__header-title-container { + flex: 1; - .confirmation__header-title { - font-size: 1.1rem; - font-weight: var(--clr-modal-title-font-weight, 200); - line-height: 1.2rem; - letter-spacing: normal; - margin: 0; - padding: 0 0.15rem 0 0; - } + .confirmation__header-title { + font-size: 1.1rem; + font-weight: var(--clr-modal-title-font-weight, 200); + line-height: 1.2rem; + letter-spacing: normal; + margin: 0; + padding: 0 0.15rem 0 0; } + } - .confirmation__header-close-btn { - margin-top: -0.05rem; - margin-right: -0.25rem; - font-size: 1.3rem; - line-height: 1.2rem; - padding: 0; - cursor: pointer; - background: 0 0; - border: 0; - -webkit-appearance: none; - float: right; - transition: color linear 0.2s; - font-weight: 200; - text-shadow: none; - color: var(--clr-close-color--normal, #8c8c8c); + .confirmation__header-close-btn { + margin-top: -0.05rem; + margin-right: -0.25rem; + font-size: 1.3rem; + line-height: 1.2rem; + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; + -webkit-appearance: none; + float: right; + transition: color linear 0.2s; + font-weight: 200; + text-shadow: none; + color: var(--clr-close-color--normal, #8c8c8c); - cds-icon { - fill: var(--clr-modal-close-color, #8c8c8c); - height: 1.2rem; - width: 1.2rem; - } + cds-icon { + fill: var(--clr-modal-close-color, #8c8c8c); + height: 1.2rem; + width: 1.2rem; } + } } .confirmation__body-container { - padding: 1.2rem 0.5rem 0 0.5rem; + padding: 1.2rem 0.5rem 0 0.5rem; - .confirmation__body-message { - margin: 0; - } + .confirmation__body-message { + margin: 0; + } - .confirmation__body-form { - padding: 0; - margin: 0; - } + .confirmation__body-form { + padding: 0; + margin: 0; + } } .confirmation__footer-container { - display: flex; - justify-content: right; - padding: 1.2rem 0.5rem 0.5rem 0.5rem; + display: flex; + justify-content: right; + padding: 1.2rem 0.5rem 0.5rem 0.5rem; - button { - margin: 0; + button { + margin: 0; - &.confirmation__footer-cancel-btn { - margin-right: 0.6rem; - } + &.confirmation__footer-cancel-btn { + margin-right: 0.6rem; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.spec.ts index bce44f2f6a..5615e43010 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.spec.ts @@ -3,329 +3,462 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DebugElement, ViewContainerRef } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; - -import { ClarityModule } from '@clr/angular'; - -import { CallFake } from '../../../../unit-testing'; - +import { DebugElement, ViewContainerRef } from "@angular/core"; import { - ConfirmationHandler, - ConfirmationInputModel, - ConfirmationModelImpl, - ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT -} from '../../model/confirmation.model'; - -import { ConfirmationComponent } from './confirmation.component'; - -describe('ConfirmationComponent', () => { - let dummyModel: ConfirmationModelImpl; + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from "@angular/core/testing"; +import { BrowserModule, By } from "@angular/platform-browser"; +import { FormsModule } from "@angular/forms"; - let component: ConfirmationComponent; - let fixture: ComponentFixture; +import { ClarityModule } from "@clr/angular"; - beforeEach(async () => { - dummyModel = new ConfirmationModelImpl({} as ConfirmationInputModel); - dummyModel.handler.confirm = CallFake; - dummyModel.handler.dismiss = CallFake; +import { CallFake } from "../../../../unit-testing"; - await TestBed.configureTestingModule({ - imports: [BrowserModule, FormsModule, ClarityModule], - declarations: [ConfirmationComponent] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ConfirmationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); +import { + ConfirmationHandler, + ConfirmationInputModel, + ConfirmationModelImpl, + ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT, +} from "../../model/confirmation.model"; + +import { ConfirmationComponent } from "./confirmation.component"; + +describe("ConfirmationComponent", () => { + let dummyModel: ConfirmationModelImpl; + + let component: ConfirmationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + dummyModel = new ConfirmationModelImpl({} as ConfirmationInputModel); + dummyModel.handler.confirm = CallFake; + dummyModel.handler.dismiss = CallFake; + + await TestBed.configureTestingModule({ + imports: [BrowserModule, FormsModule, ClarityModule], + declarations: [ConfirmationComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + component.model = dummyModel; + }); + + it("should verify component is created", () => { + // Then + expect(component).toBeTruthy(); + }); + + describe("Properties::", () => { + describe("|viewContainerRef|", () => { + it("should verify value is populate after component is created", () => { + // Then + expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); + }); }); - afterEach(() => { - component.model = dummyModel; + describe("|model|", () => { + it("should verify default value", () => { + // Then + expect(component.model).toEqual({} as ConfirmationModelImpl); + }); }); - it('should verify component is created', () => { + describe("|doNotShowFutureConfirmation|", () => { + it("should verify default value", () => { // Then - expect(component).toBeTruthy(); + expect(component.doNotShowFutureConfirmation).toBeFalse(); + }); }); - - describe('Properties::', () => { - describe('|viewContainerRef|', () => { - it('should verify value is populate after component is created', () => { - // Then - expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); - }); - }); - - describe('|model|', () => { - it('should verify default value', () => { - // Then - expect(component.model).toEqual({} as ConfirmationModelImpl); - }); - }); - - describe('|doNotShowFutureConfirmation|', () => { - it('should verify default value', () => { - // Then - expect(component.doNotShowFutureConfirmation).toBeFalse(); - }); + }); + + describe("Template::", () => { + it("should verify will render title, message, confirm button", fakeAsync(() => { + // Then 1 + const titleElement: HTMLHeadingElement = fixture.debugElement.query( + By.css(".confirmation__header-title"), + ).nativeElement; + const messageDebugElement: DebugElement = fixture.debugElement.query( + By.css(".confirmation__body-message"), + ); + const confirmationBtnElement: HTMLButtonElement = + fixture.debugElement.query( + By.css(".confirmation__footer-confirm-btn"), + ).nativeElement; + expect(titleElement.innerHTML).toEqual(""); + expect(messageDebugElement).toBeNull(); + expect(confirmationBtnElement.innerText.trim()).toEqual(""); + + // Given + const model = new ConfirmationModelImpl({ + title: "Confirmation Test", + message: `Test message, that want explicit confirmation!`, + confirmBtnModel: { + text: "Test Confirm Btn Text", + }, + }); + model.handler.confirm = CallFake; + model.handler.dismiss = CallFake; + + // When + component.model = model; + fixture.detectChanges(); + tick(500); + + // Then + const messageElement: HTMLParagraphElement = fixture.debugElement.query( + By.css(".confirmation__body-message"), + ).nativeElement; + expect(titleElement.innerHTML).toEqual("Confirmation Test"); + expect(messageElement.innerHTML).toEqual( + "Test message, that want explicit confirmation!", + ); + expect(confirmationBtnElement.innerText.trim()).toEqual( + "Test Confirm Btn Text", + ); + expect( + fixture.debugElement.query(By.css(".confirmation__header-close-btn")), + ).toBeNull(); + expect( + fixture.debugElement.query( + By.css(".confirmation__body-checkbox-opt-out input"), + ), + ).toBeNull(); + expect( + fixture.debugElement.query(By.css(".confirmation__footer-cancel-btn")), + ).toBeNull(); + expect( + fixture.debugElement.query( + By.css(".confirmation__footer-confirm-btn-icon"), + ), + ).toBeNull(); + })); + + it(`should verify won't render message if not supplied`, fakeAsync(() => { + // Then 1 + let messageDebugElement: DebugElement = fixture.debugElement.query( + By.css(".confirmation__body-message"), + ); + expect(messageDebugElement).toBeNull(); + + // Given + const model = new ConfirmationModelImpl({ + title: "Confirmation Test", + confirmBtnModel: { + text: "Test Confirm Btn Text", + }, + }); + model.handler.confirm = CallFake; + model.handler.dismiss = CallFake; + + // When + component.model = model; + fixture.detectChanges(); + tick(500); + + // Then + messageDebugElement = fixture.debugElement.query( + By.css(".confirmation__body-message"), + ); + expect(messageDebugElement).toBeNull(); + })); + + it("should verify will render title, close button, message, opt-out checkbox, cancel and confirm buttons with icons", fakeAsync(() => { + // Then 1 + const titleElement: HTMLHeadingElement = fixture.debugElement.query( + By.css(".confirmation__header-title"), + ).nativeElement; + const messageDebugElement: DebugElement = fixture.debugElement.query( + By.css(".confirmation__body-message"), + ); + const confirmationBtnElement: HTMLButtonElement = + fixture.debugElement.query( + By.css(".confirmation__footer-confirm-btn"), + ).nativeElement; + expect(titleElement.innerHTML).toEqual(""); + expect(messageDebugElement).toBeNull(); + expect(confirmationBtnElement.innerText.trim()).toEqual(""); + expect( + fixture.debugElement.query(By.css(".confirmation__header-close-btn")), + ).toBeNull(); + expect( + fixture.debugElement.query( + By.css(".confirmation__body-checkbox-opt-out input"), + ), + ).toBeNull(); + expect( + fixture.debugElement.query(By.css(".confirmation__footer-cancel-btn")), + ).toBeNull(); + expect( + fixture.debugElement.query( + By.css(".confirmation__footer-confirm-btn-icon"), + ), + ).toBeNull(); + + // Given + const model = new ConfirmationModelImpl({ + title: "
    Confirmation Test
    ", + message: `Test message, that want explicit confirmation!`, + closable: true, + optionDoNotShowFutureConfirmation: true, + cancelBtnModel: { + text: "Test Cancel Btn Text Case 1", + iconShape: "angle", + iconPosition: "right", + iconDirection: "left", + iconBadge: "warning", + iconInverse: true, + iconSize: "lg", + iconSolid: true, + iconStatus: "danger", + }, + confirmBtnModel: { + text: "Test Confirm Btn Text Case 1", + iconShape: "pop-out", + iconPosition: "left", + iconDirection: "down", + iconBadge: "info", + iconInverse: true, + iconSize: "md", + iconSolid: true, + iconStatus: "warning", + }, + }); + model.handler.confirm = CallFake; + model.handler.dismiss = CallFake; + + // When + component.model = model; + fixture.detectChanges(); + tick(500); + + // Then + const messageElement: HTMLParagraphElement = fixture.debugElement.query( + By.css(".confirmation__body-message"), + ).nativeElement; + expect(titleElement.innerHTML).toEqual("
    Confirmation Test
    "); + expect(messageElement.innerHTML).toEqual( + "Test message, that want explicit confirmation!", + ); + expect(confirmationBtnElement.innerText.trim()).toEqual( + "Test Confirm Btn Text Case 1", + ); + + // close button + const closeDebugElement = fixture.debugElement.query( + By.css(".confirmation__header-close-btn"), + ); + const closeIconElement: HTMLElement = closeDebugElement.query( + By.css("clr-icon"), + ).nativeElement; + expect(closeDebugElement.nativeElement).toBeDefined(); + expect(closeIconElement.getAttribute("shape")).toEqual("window-close"); + + // confirmation + const checkboxElement = fixture.debugElement.query( + By.css(".confirmation__body-checkbox-opt-out input"), + ); + expect(checkboxElement.nativeElement).toBeDefined(); + const labelElement: HTMLLabelElement = fixture.debugElement.query( + By.css(".confirmation__body-checkbox-wrapper label"), + ).nativeElement; + expect(labelElement.innerText).toEqual("Do not show this message again."); + + // cancel button + const cancelButtonDebugElement = fixture.debugElement.query( + By.css(".confirmation__footer-cancel-btn"), + ); + const cancelButtonElement: HTMLButtonElement = + cancelButtonDebugElement.nativeElement; + expect(cancelButtonElement).toBeDefined(); + expect(cancelButtonElement.innerText).toEqual( + "Test Cancel Btn Text Case 1", + ); + const buttonCancelIconElement: HTMLElement = + cancelButtonDebugElement.query( + By.css(".confirmation__footer-confirm-btn-icon"), + ).nativeElement; + expect(buttonCancelIconElement).toBeDefined(); + expect(buttonCancelIconElement.getAttribute("shape")).toEqual("angle"); + expect(buttonCancelIconElement.getAttribute("direction")).toEqual("left"); + expect(buttonCancelIconElement.getAttribute("size")).toEqual("lg"); + expect(buttonCancelIconElement.getAttribute("solid")).toEqual("true"); + expect(buttonCancelIconElement.getAttribute("inverse")).toEqual("true"); + expect(buttonCancelIconElement.getAttribute("status")).toEqual("danger"); + expect(buttonCancelIconElement.getAttribute("badge")).toEqual("warning"); + + // confirm button + const confirmButtonDebugElement = fixture.debugElement.query( + By.css(".confirmation__footer-confirm-btn"), + ); + const confirmButtonElement: HTMLButtonElement = + confirmButtonDebugElement.nativeElement; + expect(confirmButtonElement).toBeDefined(); + expect(confirmButtonElement.innerText).toEqual( + "Test Confirm Btn Text Case 1", + ); + const buttonConfirmIconElement: HTMLElement = + confirmButtonDebugElement.query( + By.css(".confirmation__footer-confirm-btn-icon"), + ).nativeElement; + expect(buttonConfirmIconElement).toBeDefined(); + expect(buttonConfirmIconElement.getAttribute("shape")).toEqual("pop-out"); + expect(buttonConfirmIconElement.getAttribute("direction")).toEqual( + "down", + ); + expect(buttonConfirmIconElement.getAttribute("size")).toEqual("md"); + expect(buttonConfirmIconElement.getAttribute("solid")).toEqual("true"); + expect(buttonConfirmIconElement.getAttribute("inverse")).toEqual("true"); + expect(buttonConfirmIconElement.getAttribute("status")).toEqual( + "warning", + ); + expect(buttonConfirmIconElement.getAttribute("badge")).toEqual("info"); + + // footer buttons position, first cancel then confirm + expect( + cancelButtonElement.nextElementSibling.classList.contains( + "confirmation__footer-confirm-btn", + ), + ).toBeTrue(); + })); + + const parameters: Array<[string, boolean, string, string, any]> = [ + [ + "close", + false, + ".confirmation__header-close-btn", + "dismiss", + "Confirmation canceled on User behalf", + ], + [ + "cancel", + false, + ".confirmation__footer-cancel-btn", + "dismiss", + "Confirmation canceled on User behalf", + ], + [ + "confirm", + false, + ".confirmation__footer-confirm-btn", + "confirm", + { doNotShowFutureConfirmation: false }, + ], + [ + "close", + true, + ".confirmation__header-close-btn", + "dismiss", + "Confirmation canceled on User behalf", + ], + [ + "cancel", + true, + ".confirmation__footer-cancel-btn", + "dismiss", + "Confirmation canceled on User behalf", + ], + [ + "confirm", + true, + ".confirmation__footer-confirm-btn", + "confirm", + { doNotShowFutureConfirmation: true }, + ], + ]; + for (const [ + context, + isCheckedOptOut, + selector, + handlerMethod, + assertion, + ] of parameters) { + it(`should verify ${context} view will invoke ${handlerMethod} handler method and opt-out is ${ + isCheckedOptOut as any as string + }`, fakeAsync(() => { + // Given + const handlerStub = jasmine.createSpyObj( + "handlerStub", + ["confirm", "dismiss"], + ); + const model = new ConfirmationModelImpl({ + title: "Confirmation title", + message: `Confirmation message`, + closable: true, + optionDoNotShowFutureConfirmation: true, + cancelBtnModel: { + text: "Cancel", + }, + confirmBtnModel: { + text: "Confirm", + iconShape: "pop-out", + }, }); - }); + // @ts-ignore + model["handler"] = handlerStub; // eslint-disable-line @typescript-eslint/dot-notation - describe('Template::', () => { - it('should verify will render title, message, confirm button', fakeAsync(() => { - // Then 1 - const titleElement: HTMLHeadingElement = fixture.debugElement.query(By.css('.confirmation__header-title')).nativeElement; - const messageDebugElement: DebugElement = fixture.debugElement.query(By.css('.confirmation__body-message')); - const confirmationBtnElement: HTMLButtonElement = fixture.debugElement.query( - By.css('.confirmation__footer-confirm-btn') - ).nativeElement; - expect(titleElement.innerHTML).toEqual(''); - expect(messageDebugElement).toBeNull(); - expect(confirmationBtnElement.innerText.trim()).toEqual(''); - - // Given - const model = new ConfirmationModelImpl({ - title: 'Confirmation Test', - message: `Test message, that want explicit confirmation!`, - confirmBtnModel: { - text: 'Test Confirm Btn Text' - } - }); - model.handler.confirm = CallFake; - model.handler.dismiss = CallFake; - - // When - component.model = model; - fixture.detectChanges(); - tick(500); - - // Then - const messageElement: HTMLParagraphElement = fixture.debugElement.query(By.css('.confirmation__body-message')).nativeElement; - expect(titleElement.innerHTML).toEqual('Confirmation Test'); - expect(messageElement.innerHTML).toEqual('Test message, that want explicit confirmation!'); - expect(confirmationBtnElement.innerText.trim()).toEqual('Test Confirm Btn Text'); - expect(fixture.debugElement.query(By.css('.confirmation__header-close-btn'))).toBeNull(); - expect(fixture.debugElement.query(By.css('.confirmation__body-checkbox-opt-out input'))).toBeNull(); - expect(fixture.debugElement.query(By.css('.confirmation__footer-cancel-btn'))).toBeNull(); - expect(fixture.debugElement.query(By.css('.confirmation__footer-confirm-btn-icon'))).toBeNull(); - })); - - it(`should verify won't render message if not supplied`, fakeAsync(() => { - // Then 1 - let messageDebugElement: DebugElement = fixture.debugElement.query(By.css('.confirmation__body-message')); - expect(messageDebugElement).toBeNull(); - - // Given - const model = new ConfirmationModelImpl({ - title: 'Confirmation Test', - confirmBtnModel: { - text: 'Test Confirm Btn Text' - } - }); - model.handler.confirm = CallFake; - model.handler.dismiss = CallFake; - - // When - component.model = model; - fixture.detectChanges(); - tick(500); - - // Then - messageDebugElement = fixture.debugElement.query(By.css('.confirmation__body-message')); - expect(messageDebugElement).toBeNull(); - })); - - it('should verify will render title, close button, message, opt-out checkbox, cancel and confirm buttons with icons', fakeAsync(() => { - // Then 1 - const titleElement: HTMLHeadingElement = fixture.debugElement.query(By.css('.confirmation__header-title')).nativeElement; - const messageDebugElement: DebugElement = fixture.debugElement.query(By.css('.confirmation__body-message')); - const confirmationBtnElement: HTMLButtonElement = fixture.debugElement.query( - By.css('.confirmation__footer-confirm-btn') - ).nativeElement; - expect(titleElement.innerHTML).toEqual(''); - expect(messageDebugElement).toBeNull(); - expect(confirmationBtnElement.innerText.trim()).toEqual(''); - expect(fixture.debugElement.query(By.css('.confirmation__header-close-btn'))).toBeNull(); - expect(fixture.debugElement.query(By.css('.confirmation__body-checkbox-opt-out input'))).toBeNull(); - expect(fixture.debugElement.query(By.css('.confirmation__footer-cancel-btn'))).toBeNull(); - expect(fixture.debugElement.query(By.css('.confirmation__footer-confirm-btn-icon'))).toBeNull(); - - // Given - const model = new ConfirmationModelImpl({ - title: '
    Confirmation Test
    ', - message: `Test message, that want explicit confirmation!`, - closable: true, - optionDoNotShowFutureConfirmation: true, - cancelBtnModel: { - text: 'Test Cancel Btn Text Case 1', - iconShape: 'angle', - iconPosition: 'right', - iconDirection: 'left', - iconBadge: 'warning', - iconInverse: true, - iconSize: 'lg', - iconSolid: true, - iconStatus: 'danger' - }, - confirmBtnModel: { - text: 'Test Confirm Btn Text Case 1', - iconShape: 'pop-out', - iconPosition: 'left', - iconDirection: 'down', - iconBadge: 'info', - iconInverse: true, - iconSize: 'md', - iconSolid: true, - iconStatus: 'warning' - } - }); - model.handler.confirm = CallFake; - model.handler.dismiss = CallFake; - - // When - component.model = model; - fixture.detectChanges(); - tick(500); - - // Then - const messageElement: HTMLParagraphElement = fixture.debugElement.query(By.css('.confirmation__body-message')).nativeElement; - expect(titleElement.innerHTML).toEqual('
    Confirmation Test
    '); - expect(messageElement.innerHTML).toEqual('Test message, that want explicit confirmation!'); - expect(confirmationBtnElement.innerText.trim()).toEqual('Test Confirm Btn Text Case 1'); - - // close button - const closeDebugElement = fixture.debugElement.query(By.css('.confirmation__header-close-btn')); - const closeIconElement: HTMLElement = closeDebugElement.query(By.css('clr-icon')).nativeElement; - expect(closeDebugElement.nativeElement).toBeDefined(); - expect(closeIconElement.getAttribute('shape')).toEqual('window-close'); - - // confirmation - const checkboxElement = fixture.debugElement.query(By.css('.confirmation__body-checkbox-opt-out input')); - expect(checkboxElement.nativeElement).toBeDefined(); - const labelElement: HTMLLabelElement = fixture.debugElement.query( - By.css('.confirmation__body-checkbox-wrapper label') - ).nativeElement; - expect(labelElement.innerText).toEqual('Do not show this message again.'); - - // cancel button - const cancelButtonDebugElement = fixture.debugElement.query(By.css('.confirmation__footer-cancel-btn')); - const cancelButtonElement: HTMLButtonElement = cancelButtonDebugElement.nativeElement; - expect(cancelButtonElement).toBeDefined(); - expect(cancelButtonElement.innerText).toEqual('Test Cancel Btn Text Case 1'); - const buttonCancelIconElement: HTMLElement = cancelButtonDebugElement.query( - By.css('.confirmation__footer-confirm-btn-icon') - ).nativeElement; - expect(buttonCancelIconElement).toBeDefined(); - expect(buttonCancelIconElement.getAttribute('shape')).toEqual('angle'); - expect(buttonCancelIconElement.getAttribute('direction')).toEqual('left'); - expect(buttonCancelIconElement.getAttribute('size')).toEqual('lg'); - expect(buttonCancelIconElement.getAttribute('solid')).toEqual('true'); - expect(buttonCancelIconElement.getAttribute('inverse')).toEqual('true'); - expect(buttonCancelIconElement.getAttribute('status')).toEqual('danger'); - expect(buttonCancelIconElement.getAttribute('badge')).toEqual('warning'); - - // confirm button - const confirmButtonDebugElement = fixture.debugElement.query(By.css('.confirmation__footer-confirm-btn')); - const confirmButtonElement: HTMLButtonElement = confirmButtonDebugElement.nativeElement; - expect(confirmButtonElement).toBeDefined(); - expect(confirmButtonElement.innerText).toEqual('Test Confirm Btn Text Case 1'); - const buttonConfirmIconElement: HTMLElement = confirmButtonDebugElement.query( - By.css('.confirmation__footer-confirm-btn-icon') - ).nativeElement; - expect(buttonConfirmIconElement).toBeDefined(); - expect(buttonConfirmIconElement.getAttribute('shape')).toEqual('pop-out'); - expect(buttonConfirmIconElement.getAttribute('direction')).toEqual('down'); - expect(buttonConfirmIconElement.getAttribute('size')).toEqual('md'); - expect(buttonConfirmIconElement.getAttribute('solid')).toEqual('true'); - expect(buttonConfirmIconElement.getAttribute('inverse')).toEqual('true'); - expect(buttonConfirmIconElement.getAttribute('status')).toEqual('warning'); - expect(buttonConfirmIconElement.getAttribute('badge')).toEqual('info'); - - // footer buttons position, first cancel then confirm - expect(cancelButtonElement.nextElementSibling.classList.contains('confirmation__footer-confirm-btn')).toBeTrue(); - })); - - const parameters: Array<[string, boolean, string, string, any]> = [ - ['close', false, '.confirmation__header-close-btn', 'dismiss', 'Confirmation canceled on User behalf'], - ['cancel', false, '.confirmation__footer-cancel-btn', 'dismiss', 'Confirmation canceled on User behalf'], - ['confirm', false, '.confirmation__footer-confirm-btn', 'confirm', { doNotShowFutureConfirmation: false }], - ['close', true, '.confirmation__header-close-btn', 'dismiss', 'Confirmation canceled on User behalf'], - ['cancel', true, '.confirmation__footer-cancel-btn', 'dismiss', 'Confirmation canceled on User behalf'], - ['confirm', true, '.confirmation__footer-confirm-btn', 'confirm', { doNotShowFutureConfirmation: true }] - ]; - for (const [context, isCheckedOptOut, selector, handlerMethod, assertion] of parameters) { - it(`should verify ${context} view will invoke ${handlerMethod} handler method and opt-out is ${ - isCheckedOptOut as any as string - }`, fakeAsync(() => { - // Given - const handlerStub = jasmine.createSpyObj('handlerStub', ['confirm', 'dismiss']); - const model = new ConfirmationModelImpl({ - title: 'Confirmation title', - message: `Confirmation message`, - closable: true, - optionDoNotShowFutureConfirmation: true, - cancelBtnModel: { - text: 'Cancel' - }, - confirmBtnModel: { - text: 'Confirm', - iconShape: 'pop-out' - } - }); - // @ts-ignore - model['handler'] = handlerStub; // eslint-disable-line @typescript-eslint/dot-notation - - // When 1 - component.model = model; - fixture.detectChanges(); - tick(500); - if (isCheckedOptOut) { - const optOutCheckbox: HTMLInputElement = fixture.debugElement.query( - By.css('.confirmation__body-checkbox-opt-out input') - ).nativeElement; - optOutCheckbox.click(); - tick(500); - } - const actionElement: HTMLButtonElement = fixture.debugElement.query(By.css(selector)).nativeElement; - actionElement.click(); - tick(500); - - // Then - expect(handlerStub[handlerMethod]).toHaveBeenCalledTimes(1); - expect(handlerStub[handlerMethod]).toHaveBeenCalledWith(assertion); - })); + // When 1 + component.model = model; + fixture.detectChanges(); + tick(500); + if (isCheckedOptOut) { + const optOutCheckbox: HTMLInputElement = fixture.debugElement.query( + By.css(".confirmation__body-checkbox-opt-out input"), + ).nativeElement; + optOutCheckbox.click(); + tick(500); } + const actionElement: HTMLButtonElement = fixture.debugElement.query( + By.css(selector), + ).nativeElement; + actionElement.click(); + tick(500); - it('should verify without user interaction when component is forcefully destroyed will throw Error with code', fakeAsync(() => { - // Given - const handlerStub = jasmine.createSpyObj('handlerStub', ['confirm', 'dismiss']); - const model = new ConfirmationModelImpl({ - title: 'Confirmation title error thrown', - message: `Confirmation message error thrown`, - cancelBtnModel: { - text: null - }, - confirmBtnModel: { - text: 'Confirm', - iconShape: 'pop-out' - } - }); - // @ts-ignore - model['handler'] = handlerStub; // eslint-disable-line @typescript-eslint/dot-notation - - // When 1 - component.model = model; - fixture.detectChanges(); - tick(500); - fixture.destroy(); - - // Then - expect(handlerStub.dismiss).toHaveBeenCalledWith(new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT)); - })); - }); + // Then + expect(handlerStub[handlerMethod]).toHaveBeenCalledTimes(1); + expect(handlerStub[handlerMethod]).toHaveBeenCalledWith(assertion); + })); + } + + it("should verify without user interaction when component is forcefully destroyed will throw Error with code", fakeAsync(() => { + // Given + const handlerStub = jasmine.createSpyObj( + "handlerStub", + ["confirm", "dismiss"], + ); + const model = new ConfirmationModelImpl({ + title: "Confirmation title error thrown", + message: `Confirmation message error thrown`, + cancelBtnModel: { + text: null, + }, + confirmBtnModel: { + text: "Confirm", + iconShape: "pop-out", + }, + }); + // @ts-ignore + model["handler"] = handlerStub; // eslint-disable-line @typescript-eslint/dot-notation + + // When 1 + component.model = model; + fixture.detectChanges(); + tick(500); + fixture.destroy(); + + // Then + expect(handlerStub.dismiss).toHaveBeenCalledWith( + new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT), + ); + })); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.ts index 742f459417..9a46ff13fd 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/confirmation/confirmation.component.ts @@ -3,9 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, OnDestroy, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + Input, + OnDestroy, + ViewChild, + ViewContainerRef, +} from "@angular/core"; -import { ConfirmationModelImpl, ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT } from '../../model/confirmation.model'; +import { + ConfirmationModelImpl, + ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT, +} from "../../model/confirmation.model"; /** * ** Confirmation Component that is created for every confirmation as in {@link ConfirmationService}. @@ -14,73 +23,78 @@ import { ConfirmationModelImpl, ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COM * is managed from {@link ConfirmationService}. */ @Component({ - selector: 'shared-confirmation', - templateUrl: './confirmation.component.html', - styleUrls: ['./confirmation.component.scss'] + selector: "shared-confirmation", + templateUrl: "./confirmation.component.html", + styleUrls: ["./confirmation.component.scss"], }) export class ConfirmationComponent implements OnDestroy { - /** - * ** ViewContainerRef reference that is used as point where Message component could be inserted, - * and that Component is provided from invoker. - * - * - Reference is contextual and unique for every confirmation message. - */ - @ViewChild('confirmationMessageComponent', { read: ViewContainerRef, static: true }) - public readonly viewContainerRef: ViewContainerRef; + /** + * ** ViewContainerRef reference that is used as point where Message component could be inserted, + * and that Component is provided from invoker. + * + * - Reference is contextual and unique for every confirmation message. + */ + @ViewChild("confirmationMessageComponent", { + read: ViewContainerRef, + static: true, + }) + public readonly viewContainerRef: ViewContainerRef; - /** - * ** Model provided to component - * - * - It instructs view rendering and behavior. - * - Provided handler is invoked when User interact with Confirmation view, whether its confirm or cancel (dismiss). - */ - @Input() model: ConfirmationModelImpl = {} as ConfirmationModelImpl; + /** + * ** Model provided to component + * + * - It instructs view rendering and behavior. + * - Provided handler is invoked when User interact with Confirmation view, whether its confirm or cancel (dismiss). + */ + @Input() model: ConfirmationModelImpl = {} as ConfirmationModelImpl; - /** - * ** Whether User opt-out from future confirmation with the same context. - * - * - Only when model instructs such field to be rendered, otherwise it's by default FALSE. - */ - doNotShowFutureConfirmation = false; + /** + * ** Whether User opt-out from future confirmation with the same context. + * + * - Only when model instructs such field to be rendered, otherwise it's by default FALSE. + */ + doNotShowFutureConfirmation = false; - /** - * ** Flag that is set to TRUE when User interact with Confirmation view, whether it is Confirm or Dismiss. - * @private - */ - private _componentInteracted = false; + /** + * ** Flag that is set to TRUE when User interact with Confirmation view, whether it is Confirm or Dismiss. + * @private + */ + private _componentInteracted = false; - /** - * ** User give confirmation, propagate model to invoker. - */ - confirm($event: MouseEvent): void { - $event.preventDefault(); + /** + * ** User give confirmation, propagate model to invoker. + */ + confirm($event: MouseEvent): void { + $event.preventDefault(); - this._componentInteracted = true; + this._componentInteracted = true; - this.model.handler.confirm({ - doNotShowFutureConfirmation: this.doNotShowFutureConfirmation - }); - } - - /** - * ** User cancel (dismiss) confirmation, propagate reason to invoker that it's on User behalf. - */ - cancel($event: MouseEvent): void { - $event.preventDefault(); + this.model.handler.confirm({ + doNotShowFutureConfirmation: this.doNotShowFutureConfirmation, + }); + } - this._componentInteracted = true; + /** + * ** User cancel (dismiss) confirmation, propagate reason to invoker that it's on User behalf. + */ + cancel($event: MouseEvent): void { + $event.preventDefault(); - this.model.handler.dismiss('Confirmation canceled on User behalf'); - } + this._componentInteracted = true; - /** - * @inheritDoc - */ - ngOnDestroy(): void { - if (this._componentInteracted) { - return; - } + this.model.handler.dismiss("Confirmation canceled on User behalf"); + } - this.model.handler.dismiss(new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT)); + /** + * @inheritDoc + */ + ngOnDestroy(): void { + if (this._componentInteracted) { + return; } + + this.model.handler.dismiss( + new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT), + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.html index 7627e4dc4f..4e94ad2091 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.html @@ -4,11 +4,11 @@ --> - + diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.scss index 6474f4db91..9aa6a15b90 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.scss @@ -4,33 +4,33 @@ */ :host { - ::ng-deep { - .modal-dialog { - width: 44rem; + ::ng-deep { + .modal-dialog { + width: 44rem; - .modal-content { - padding: 0.7rem 0.7rem 0.7rem 0.7rem; - } - } + .modal-content { + padding: 0.7rem 0.7rem 0.7rem 0.7rem; + } + } - .modal-header--accessible { - display: none; - } + .modal-header--accessible { + display: none; + } - .modal-body-wrapper { - overflow: visible; - } + .modal-body-wrapper { + overflow: visible; + } - .confirmation-container__body-container { - overflow-y: auto; - overflow-x: hidden; - max-height: 70vh; - } + .confirmation-container__body-container { + overflow-y: auto; + overflow-x: hidden; + max-height: 70vh; + } - shared-confirmation { - &:not(:first-child) { - margin-top: 0.7rem; - } - } + shared-confirmation { + &:not(:first-child) { + margin-top: 0.7rem; + } } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.spec.ts index 0ce288241a..44f2de19d3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.spec.ts @@ -3,48 +3,48 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA, ViewContainerRef } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConfirmationContainerComponent } from './confirmation-container.component'; - -describe('ConfirmationContainerComponent', () => { - let component: ConfirmationContainerComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [BrowserModule], - declarations: [ConfirmationContainerComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ConfirmationContainerComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should verify component is created', () => { +import { CUSTOM_ELEMENTS_SCHEMA, ViewContainerRef } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ConfirmationContainerComponent } from "./confirmation-container.component"; + +describe("ConfirmationContainerComponent", () => { + let component: ConfirmationContainerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BrowserModule], + declarations: [ConfirmationContainerComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmationContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should verify component is created", () => { + // Then + expect(component).toBeTruthy(); + }); + + describe("Properties::", () => { + describe("|viewContainerRef|", () => { + it("should verify value is populate after component is created", () => { // Then - expect(component).toBeTruthy(); + expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); + }); }); - describe('Properties::', () => { - describe('|viewContainerRef|', () => { - it('should verify value is populate after component is created', () => { - // Then - expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); - }); - }); - - describe('|opened|', () => { - it('should verify value is false', () => { - // Then - expect(component.open).toBeFalse(); - }); - }); + describe("|opened|", () => { + it("should verify value is false", () => { + // Then + expect(component.open).toBeFalse(); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.ts index a020a20bb0..1b77c591db 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/container/confirmation-container.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, Input, ViewChild, ViewContainerRef } from "@angular/core"; /** * ** Confirmation Container Component is created only once upon {@link ConfirmationService} initialization @@ -13,24 +13,24 @@ import { Component, Input, ViewChild, ViewContainerRef } from '@angular/core'; * is stored as ViewContainerRef in {@link ConfirmationService} for lifetime of the service as singleton. */ @Component({ - selector: 'shared-confirmation-container', - templateUrl: './confirmation-container.component.html', - styleUrls: ['./confirmation-container.component.scss'] + selector: "shared-confirmation-container", + templateUrl: "./confirmation-container.component.html", + styleUrls: ["./confirmation-container.component.scss"], }) export class ConfirmationContainerComponent { - /** - * ** ViewContainerRef reference that is used as point where {@link ConfirmationService} insert contextual {@link ConfirmationComponent} - * one for every single confirmation ask when invokers call {@link ConfirmationService.confirm}. - * - * - Reference is singleton, and it is retrieved only once upon {@link ConfirmationService} initialization. - */ - @ViewChild('insertionPoint', { read: ViewContainerRef }) - public readonly viewContainerRef: ViewContainerRef; + /** + * ** ViewContainerRef reference that is used as point where {@link ConfirmationService} insert contextual {@link ConfirmationComponent} + * one for every single confirmation ask when invokers call {@link ConfirmationService.confirm}. + * + * - Reference is singleton, and it is retrieved only once upon {@link ConfirmationService} initialization. + */ + @ViewChild("insertionPoint", { read: ViewContainerRef }) + public readonly viewContainerRef: ViewContainerRef; - /** - * ** Input parameter that instructs Component to open/close modal (visualize/hide) confirmation view/s. - * - * - State is managed from {@link ConfirmationService} - */ - @Input() open = false; + /** + * ** Input parameter that instructs Component to open/close modal (visualize/hide) confirmation view/s. + * + * - State is managed from {@link ConfirmationService} + */ + @Input() open = false; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/index.ts index 1f49664ffd..0c07b62787 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/components/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './container/confirmation-container.component'; -export * from './confirmation/confirmation.component'; +export * from "./container/confirmation-container.component"; +export * from "./confirmation/confirmation.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/confirmation.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/confirmation.module.ts index 2d08bdd2ff..d3ead997bf 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/confirmation.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/confirmation.module.ts @@ -3,13 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; +import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule } from "@angular/forms"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { ConfirmationComponent, ConfirmationContainerComponent } from './components'; +import { + ConfirmationComponent, + ConfirmationContainerComponent, +} from "./components"; /** * ** Confirmation module @@ -17,8 +20,8 @@ import { ConfirmationComponent, ConfirmationContainerComponent } from './compone * @author gorankokin */ @NgModule({ - imports: [CommonModule, ClarityModule, FormsModule], - declarations: [ConfirmationContainerComponent, ConfirmationComponent], - schemas: [NO_ERRORS_SCHEMA] + imports: [CommonModule, ClarityModule, FormsModule], + declarations: [ConfirmationContainerComponent, ConfirmationComponent], + schemas: [NO_ERRORS_SCHEMA], }) export class ConfirmationModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/index.ts index 750cec183f..aa92901aaf 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './model'; -export * from './services'; +export * from "./model"; +export * from "./services"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.spec.ts index d650d663cc..7fe33afbbd 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.spec.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ConfirmationModelImpl } from './confirmation.model'; +import { ConfirmationModelImpl } from "./confirmation.model"; -xdescribe('ConfirmationModelImpl', () => { - let model: ConfirmationModelImpl; +xdescribe("ConfirmationModelImpl", () => { + let model: ConfirmationModelImpl; }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.ts index 3f3c6d4ebd..6af07adaf2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/confirmation.model.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Type } from '@angular/core'; +import { Type } from "@angular/core"; -import { CollectionsUtil, Mutable } from '../../../utils'; +import { CollectionsUtil, Mutable } from "../../../utils"; -export const ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT = 'EC_CONFIRMATION_1000'; +export const ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT = + "EC_CONFIRMATION_1000"; /** * ** Confirmation Input Model. @@ -15,22 +16,23 @@ export const ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT = 'EC_CONFIR * - Model instance provided as input instructions for Confirmation Service, or to more specific to method {@link ConfirmationService.confirm} * - Most of the fields are optional and Model Impl provides its own defaults. */ -export interface ConfirmationInputModel extends SupportedButtonsModel, SupportedMessageModel { - /** - * ** Confirmation title. - * - * - Service render provided content as innerHTML. - * - HTML tags could be provided in string template. - */ - title?: string; - /** - * ** Whether confirmation view to render close X button in top right corner. - */ - closable?: boolean; - /** - * ** Whether confirmation view to render option for User to opt-out of showing confirmations with same context in the future. - */ - optionDoNotShowFutureConfirmation?: boolean; +export interface ConfirmationInputModel + extends SupportedButtonsModel, SupportedMessageModel { + /** + * ** Confirmation title. + * + * - Service render provided content as innerHTML. + * - HTML tags could be provided in string template. + */ + title?: string; + /** + * ** Whether confirmation view to render close X button in top right corner. + */ + closable?: boolean; + /** + * ** Whether confirmation view to render option for User to opt-out of showing confirmations with same context in the future. + */ + optionDoNotShowFutureConfirmation?: boolean; } /** @@ -39,14 +41,14 @@ export interface ConfirmationInputModel extends SupportedButtonsModel, Supported * - private model used only in the service. */ export interface ConfirmationModelExtension { - /** - * ** Model UUID. - */ - uuid: string; - /** - * ** Confirmation Handler. - */ - handler: ConfirmationHandler; + /** + * ** Model UUID. + */ + uuid: string; + /** + * ** Confirmation Handler. + */ + handler: ConfirmationHandler; } /** @@ -56,215 +58,224 @@ export interface ConfirmationModelExtension { * - it could be Component class ref with optional messageCode */ export interface SupportedMessageModel { - /** - * ** Confirmation message. - * - * - Service render provided content as innerHTML. - * - HTML tags could be provided in string template. - */ - message?: string; - /** - * ** Confirmation message component. - * - * - Service render provided component in the same place where message text is rendered. - * - Message Component takes precedence before message text. e.g. if both fields are provided, Service will render the Component. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messageComponent?: Type; - /** - * ** Confirmation message code, that would be injected to Message component in initialization time - * before first changeDetection in order to re-use same component for different messages. - */ - messageCode?: string; + /** + * ** Confirmation message. + * + * - Service render provided content as innerHTML. + * - HTML tags could be provided in string template. + */ + message?: string; + /** + * ** Confirmation message component. + * + * - Service render provided component in the same place where message text is rendered. + * - Message Component takes precedence before message text. e.g. if both fields are provided, Service will render the Component. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + messageComponent?: Type; + /** + * ** Confirmation message code, that would be injected to Message component in initialization time + * before first changeDetection in order to re-use same component for different messages. + */ + messageCode?: string; } /** * ** Supported Confirmation View Buttons. */ export interface SupportedButtonsModel { - /** - * ** Model for Confirmation Cancel Button. - * - * - Providing Cancel button model, means this button should be rendered. - */ - cancelBtnModel?: Partial; - /** - * ** Model for Confirmation Confirm Button. - */ - confirmBtnModel?: ButtonModel; + /** + * ** Model for Confirmation Cancel Button. + * + * - Providing Cancel button model, means this button should be rendered. + */ + cancelBtnModel?: Partial; + /** + * ** Model for Confirmation Confirm Button. + */ + confirmBtnModel?: ButtonModel; } /** * ** Generic Button Model in Confirmation view. */ export interface ButtonModel { - /** - * ** Button text. - */ - text: string; - /** - * ** Button icon shape. - */ - iconShape?: string; - /** - * ** Button icon position. - */ - iconPosition?: 'left' | 'right'; - /** - * ** Button icon direction. - */ - iconDirection?: 'up' | 'down' | 'left' | 'right'; - /** - * ** Button icon size. - */ - iconSize?: string | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'; - /** - * ** Button icon solid. - */ - iconSolid?: boolean; - /** - * ** Button icon inverse. - */ - iconInverse?: boolean; - /** - * ** Button icon status. - */ - iconStatus?: 'info' | 'success' | 'warning' | 'danger'; - /** - * ** Button icon badge. - */ - iconBadge?: 'info' | 'success' | 'warning' | 'danger'; + /** + * ** Button text. + */ + text: string; + /** + * ** Button icon shape. + */ + iconShape?: string; + /** + * ** Button icon position. + */ + iconPosition?: "left" | "right"; + /** + * ** Button icon direction. + */ + iconDirection?: "up" | "down" | "left" | "right"; + /** + * ** Button icon size. + */ + iconSize?: string | "xs" | "sm" | "md" | "lg" | "xl" | "xxl"; + /** + * ** Button icon solid. + */ + iconSolid?: boolean; + /** + * ** Button icon inverse. + */ + iconInverse?: boolean; + /** + * ** Button icon status. + */ + iconStatus?: "info" | "success" | "warning" | "danger"; + /** + * ** Button icon badge. + */ + iconBadge?: "info" | "success" | "warning" | "danger"; } /** * ** Confirmation Model implementation that leverage input model and model extension. */ -export class ConfirmationModelImpl implements ConfirmationInputModel, ConfirmationModelExtension { - /** - * @inheritDoc - */ - readonly uuid: string; - /** - * @inheritDoc - * - * - By default it's empty. - */ - readonly title?: string; - /** - * @inheritDoc - * - * - By default it's empty. - */ - readonly message?: string; - /** - * @inheritDoc - * - * - By default is undefined. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly messageComponent?: Type; - /** - * @inheritDoc - * - * - By default it's empty. - */ - messageCode?: string; - /** - * @inheritDoc - * - * - By default its FALSE. - */ - readonly closable?: boolean; - /** - * @inheritDoc - * - * - By default its FALSE. - */ - readonly optionDoNotShowFutureConfirmation?: boolean; - /** - * @inheritDoc - * - * - By default its text is Cancel. - */ - readonly cancelBtnModel?: Readonly; - /** - * @inheritDoc - * - * - By default its text is Confirm. - */ - readonly confirmBtnModel?: Readonly; - /** - * @inheritDoc - */ - readonly handler: ConfirmationHandler; +export class ConfirmationModelImpl + implements ConfirmationInputModel, ConfirmationModelExtension +{ + /** + * @inheritDoc + */ + readonly uuid: string; + /** + * @inheritDoc + * + * - By default it's empty. + */ + readonly title?: string; + /** + * @inheritDoc + * + * - By default it's empty. + */ + readonly message?: string; + /** + * @inheritDoc + * + * - By default is undefined. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly messageComponent?: Type; + /** + * @inheritDoc + * + * - By default it's empty. + */ + messageCode?: string; + /** + * @inheritDoc + * + * - By default its FALSE. + */ + readonly closable?: boolean; + /** + * @inheritDoc + * + * - By default its FALSE. + */ + readonly optionDoNotShowFutureConfirmation?: boolean; + /** + * @inheritDoc + * + * - By default its text is Cancel. + */ + readonly cancelBtnModel?: Readonly; + /** + * @inheritDoc + * + * - By default its text is Confirm. + */ + readonly confirmBtnModel?: Readonly; + /** + * @inheritDoc + */ + readonly handler: ConfirmationHandler; - /** - * ** Constructor. - */ - constructor(model: ConfirmationInputModel) { - // assign provided model to model class fields - Object.assign(this, model ?? {}); + /** + * ** Constructor. + */ + constructor(model: ConfirmationInputModel) { + // assign provided model to model class fields + Object.assign(this, model ?? {}); - // assign UUID - this.uuid = CollectionsUtil.generateUUID(); + // assign UUID + this.uuid = CollectionsUtil.generateUUID(); - // initialize handler ref - if (CollectionsUtil.isNil(this.handler)) { - this.handler = { - confirm: null, - dismiss: null - }; - } + // initialize handler ref + if (CollectionsUtil.isNil(this.handler)) { + this.handler = { + confirm: null, + dismiss: null, + }; + } - // check if value exist, otherwise set to default FALSE - if (CollectionsUtil.isNil(this.closable)) { - this.closable = false; - } + // check if value exist, otherwise set to default FALSE + if (CollectionsUtil.isNil(this.closable)) { + this.closable = false; + } - // check if value exist, otherwise set to default FALSE - if (CollectionsUtil.isNil(this.optionDoNotShowFutureConfirmation)) { - this.optionDoNotShowFutureConfirmation = false; - } + // check if value exist, otherwise set to default FALSE + if (CollectionsUtil.isNil(this.optionDoNotShowFutureConfirmation)) { + this.optionDoNotShowFutureConfirmation = false; + } - // confirm button model - this._assignButtonModelDefaults('confirmBtnModel', 'Confirm'); + // confirm button model + this._assignButtonModelDefaults("confirmBtnModel", "Confirm"); - // cancel button model - if (CollectionsUtil.isObjectNotNull(this.cancelBtnModel)) { - this._assignButtonModelDefaults('cancelBtnModel', 'Cancel'); - } else { - this.cancelBtnModel = null; - } + // cancel button model + if (CollectionsUtil.isObjectNotNull(this.cancelBtnModel)) { + this._assignButtonModelDefaults("cancelBtnModel", "Cancel"); + } else { + this.cancelBtnModel = null; } + } - private _assignButtonModelDefaults(modelKey: keyof SupportedButtonsModel, defaultText: string): void { - // when there is no model set default text only, and return flow to invoker - if (CollectionsUtil.isNil(this[modelKey])) { - (this[modelKey] as Mutable) = { - text: defaultText - }; + private _assignButtonModelDefaults( + modelKey: keyof SupportedButtonsModel, + defaultText: string, + ): void { + // when there is no model set default text only, and return flow to invoker + if (CollectionsUtil.isNil(this[modelKey])) { + (this[modelKey] as Mutable) = { + text: defaultText, + }; - return; - } + return; + } - // if model exist but there is no text, set default one, and continue further - if (!CollectionsUtil.isStringWithContent(this[modelKey].text)) { - (this[modelKey] as Mutable).text = defaultText; - } + // if model exist but there is no text, set default one, and continue further + if (!CollectionsUtil.isStringWithContent(this[modelKey].text)) { + (this[modelKey] as Mutable).text = defaultText; + } - // if model exist check if there is no icon shape, and return flow to invoker - if (!this[modelKey].iconShape) { - (this[modelKey] as Mutable).iconShape = null; - (this[modelKey] as Mutable).iconPosition = null; + // if model exist check if there is no icon shape, and return flow to invoker + if (!this[modelKey].iconShape) { + (this[modelKey] as Mutable).iconShape = null; + (this[modelKey] as Mutable).iconPosition = null; - return; - } + return; + } - // if model exist check if there is no position set or position is something unsupported and set default one to 'left' - if (!this[modelKey].iconPosition || (this[modelKey].iconPosition !== 'left' && this[modelKey].iconPosition !== 'right')) { - (this[modelKey] as Mutable).iconPosition = 'left'; - } + // if model exist check if there is no position set or position is something unsupported and set default one to 'left' + if ( + !this[modelKey].iconPosition || + (this[modelKey].iconPosition !== "left" && + this[modelKey].iconPosition !== "right") + ) { + (this[modelKey] as Mutable).iconPosition = "left"; } + } } /** @@ -273,22 +284,22 @@ export class ConfirmationModelImpl implements ConfirmationInputModel, Confirmati * - Returned to invoker after User confirmation. */ export interface ConfirmationOutputModel { - /** - * ** Field value of true, means User opt-out of showing confirmations with same context in the future. - */ - doNotShowFutureConfirmation: boolean; + /** + * ** Field value of true, means User opt-out of showing confirmations with same context in the future. + */ + doNotShowFutureConfirmation: boolean; } /** * ** Confirmation handler. */ export interface ConfirmationHandler { - /** - * ** Confirm method, which means User give confirmation. - */ - confirm: (value: ConfirmationOutputModel) => void; - /** - * ** Dismiss (reject) method, which means User don't give confirmation. - */ - dismiss: (reason?: string | Error) => void; + /** + * ** Confirm method, which means User give confirmation. + */ + confirm: (value: ConfirmationOutputModel) => void; + /** + * ** Dismiss (reject) method, which means User don't give confirmation. + */ + dismiss: (reason?: string | Error) => void; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/index.ts index 9a8fdcd382..f7d9070520 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/model/index.ts @@ -3,4 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { ConfirmationOutputModel, ConfirmationInputModel } from './confirmation.model'; +export { + ConfirmationOutputModel, + ConfirmationInputModel, +} from "./confirmation.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/public-api.ts index 294e5adf86..9db582a439 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/public-api.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { ConfirmationInputModel, ConfirmationOutputModel } from './model'; -export { ConfirmationService } from './services'; -export { ConfirmationModule } from './confirmation.module'; +export { ConfirmationInputModel, ConfirmationOutputModel } from "./model"; +export { ConfirmationService } from "./services"; +export { ConfirmationModule } from "./confirmation.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.spec.ts index c35d1c312c..d5a5fedc85 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.spec.ts @@ -5,658 +5,863 @@ /* eslint-disable @typescript-eslint/dot-notation */ -import { ChangeDetectorRef, Component, ComponentRef, Input, ViewContainerRef, ViewRef } from '@angular/core'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { + ChangeDetectorRef, + Component, + ComponentRef, + Input, + ViewContainerRef, + ViewRef, +} from "@angular/core"; +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { DynamicComponentsService } from '../../dynamic-components'; +import { DynamicComponentsService } from "../../dynamic-components"; import { - ConfirmationInputModel, - ConfirmationModelImpl, - ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT -} from '../model/confirmation.model'; - -import { ConfirmationComponent, ConfirmationContainerComponent } from '../components'; + ConfirmationInputModel, + ConfirmationModelImpl, + ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT, +} from "../model/confirmation.model"; -import { ConfirmationService } from './confirmation.service'; +import { + ConfirmationComponent, + ConfirmationContainerComponent, +} from "../components"; + +import { ConfirmationService } from "./confirmation.service"; + +describe("ConfirmationService", () => { + let dynamicComponentsServiceStub: jasmine.SpyObj; + + let acquiredDynamicViewComponentRef: jasmine.SpyObj; + let acquiredDynamicHostViewStub: jasmine.SpyObj; + + let containerChangeDetectorRefStub: jasmine.SpyObj; + let containerViewContainerRefStub: jasmine.SpyObj; + let containerComponentRefStub: jasmine.SpyObj< + ComponentRef + >; + + let confirmationChangeDetectorRefStub: jasmine.SpyObj; + let confirmationComponentRefStub: jasmine.SpyObj< + ComponentRef + >; + let confirmationComponentHostViewStub: jasmine.SpyObj; + let confirmationComponentViewContainerRefStub: jasmine.SpyObj; + let confirmationMessageComponentRefStub: jasmine.SpyObj< + ComponentRef + >; + + let consoleErrorSpy: jasmine.Spy; + + let service: ConfirmationService; + + beforeEach(() => { + dynamicComponentsServiceStub = + jasmine.createSpyObj( + "dynamicComponentsServiceStub", + ["getUniqueViewContainerRef", "destroyUniqueViewContainerRef"], + ); + + acquiredDynamicViewComponentRef = jasmine.createSpyObj( + "acquiredDynamicViewComponentRef", + ["createComponent", "clear"], + ); + acquiredDynamicHostViewStub = jasmine.createSpyObj( + "acquiredDynamicHostViewStub", + ["destroy"], + { + destroyed: false, + }, + ); + + containerChangeDetectorRefStub = jasmine.createSpyObj( + "confirmationContainerChangeDetectorRefStub", + ["detectChanges"], + ); + containerViewContainerRefStub = jasmine.createSpyObj( + "confirmationContainerViewContainerRefStub", + ["createComponent", "clear", "indexOf", "remove"], + { + length: 0, + }, + ); + containerComponentRefStub = jasmine.createSpyObj< + ComponentRef + >("confirmationContainerComponentRefStub", ["destroy"], { + changeDetectorRef: containerChangeDetectorRefStub, + instance: { + viewContainerRef: containerViewContainerRefStub, + open: false, + }, + hostView: { + destroyed: false, + } as ViewRef, + }); -describe('ConfirmationService', () => { - let dynamicComponentsServiceStub: jasmine.SpyObj; + confirmationChangeDetectorRefStub = jasmine.createSpyObj( + "confirmationComponentChangeDetectorRefStub", + ["detectChanges"], + ); + confirmationComponentHostViewStub = jasmine.createSpyObj( + "confirmationComponentHostViewStub", + ["destroy"], + { + destroyed: false, + }, + ); + confirmationComponentViewContainerRefStub = + jasmine.createSpyObj( + "confirmationComponentViewContainerRefStub", + ["createComponent"], + ); + confirmationComponentRefStub = jasmine.createSpyObj< + ComponentRef + >("confirmationComponentRefStub", ["destroy"], { + changeDetectorRef: confirmationChangeDetectorRefStub, + instance: { + model: null, + doNotShowFutureConfirmation: false, + viewContainerRef: + confirmationComponentViewContainerRefStub as ViewContainerRef, + } as ConfirmationComponent, + hostView: confirmationComponentHostViewStub, + }); + confirmationMessageComponentRefStub = jasmine.createSpyObj< + ComponentRef + >("confirmationMessageComponentRefStub", ["destroy"], { + instance: { + messageCode: undefined, + } as DummyMessageComponent, + }); - let acquiredDynamicViewComponentRef: jasmine.SpyObj; - let acquiredDynamicHostViewStub: jasmine.SpyObj; + consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + + TestBed.configureTestingModule({ + declarations: [DummyMessageComponent], + providers: [ + { + provide: DynamicComponentsService, + useValue: dynamicComponentsServiceStub, + }, + ConfirmationService, + ], + }); - let containerChangeDetectorRefStub: jasmine.SpyObj; - let containerViewContainerRefStub: jasmine.SpyObj; - let containerComponentRefStub: jasmine.SpyObj>; + service = TestBed.inject(ConfirmationService); + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + expect(service).toBeInstanceOf(ConfirmationService); + expect(service).toBeInstanceOf(TaurusObject); + }); + + describe("Statics::", () => { + describe("Properties::", () => { + describe("|CLASS_NAME|", () => { + it("should verify the value", () => { + // Then + expect(ConfirmationService.CLASS_NAME).toEqual("ConfirmationService"); + }); + }); + }); + }); - let confirmationChangeDetectorRefStub: jasmine.SpyObj; - let confirmationComponentRefStub: jasmine.SpyObj>; - let confirmationComponentHostViewStub: jasmine.SpyObj; - let confirmationComponentViewContainerRefStub: jasmine.SpyObj; - let confirmationMessageComponentRefStub: jasmine.SpyObj>; + describe("Properties::", () => { + describe("|objectUUID|", () => { + it("should verify value is ConfirmationService", () => { + // Then + expect(/^ConfirmationService/.test(service.objectUUID)).toBeTrue(); + }); + }); + }); + + describe("Methods::", () => { + const setupTestPrerequisites = () => { + const inputModel: ConfirmationInputModel = { + title: "Confirm title", + message: "Confirm message", + }; + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ + uuid: CollectionsUtil.generateUUID(), + viewContainerRef: acquiredDynamicViewComponentRef, + hostView: acquiredDynamicHostViewStub, + }); + acquiredDynamicViewComponentRef.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + confirmationComponentRefStub, + ); + containerViewContainerRefStub.indexOf.and.returnValue(0); + + return inputModel; + }; + + describe("|initialize|", () => { + it(`should verify invoking won't throw error`, () => { + // When/Then + expect(() => service.initialize()).not.toThrowError(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + }); - let consoleErrorSpy: jasmine.Spy; + describe("|confirm|", () => { + const verifyExpectationsInsidePromise = () => { + expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith( + confirmationComponentHostViewStub, + ); + expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); + + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(3); + + expect(confirmationComponentRefStub.destroy).toHaveBeenCalledTimes(1); + + expect(containerComponentRefStub.instance.open).toBeFalse(); + }; + const verifyExpectationsOutsidePromise = ( + inputModel: ConfirmationInputModel, + verifyCreationOnly = false, + ) => { + expect( + dynamicComponentsServiceStub.getUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + + expect( + acquiredDynamicViewComponentRef.createComponent, + ).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect( + acquiredDynamicViewComponentRef.createComponent, + ).toHaveBeenCalledWith(ConfirmationContainerComponent as any); + + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(ConfirmationComponent as any); + + if (verifyCreationOnly) { + return; + } + + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(2); + expect( + confirmationChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(1); + + expect(containerComponentRefStub.instance.open).toBeTrue(); + expect(confirmationComponentRefStub.instance.model).toBeInstanceOf( + ConfirmationModelImpl, + ); + expect(confirmationComponentRefStub.instance.model.title).toEqual( + inputModel.title, + ); + expect(confirmationComponentRefStub.instance.model.message).toEqual( + inputModel.message, + ); + expect( + confirmationComponentRefStub.instance.model.messageComponent, + ).toBeUndefined(); + expect( + confirmationComponentRefStub.instance.model.messageCode, + ).toBeUndefined(); + expect(confirmationComponentRefStub.instance.model.handler).toEqual({ + confirm: jasmine.any(Function), + dismiss: jasmine.any(Function), + }); + }; - let service: ConfirmationService; + it("should verify will reject when cannot acquired ViewContainerRef from DynamicComponentsService", (done) => { + // Given + const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, cannot acquire unique ViewContainerRef where to insert confirmation Views`; + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue( + null, + ); - beforeEach(() => { - dynamicComponentsServiceStub = jasmine.createSpyObj('dynamicComponentsServiceStub', [ - 'getUniqueViewContainerRef', - 'destroyUniqueViewContainerRef' - ]); + // When/Then + service + .confirm({ + title: "Confirm title", + message: "Confirm message", + }) + .catch((reason) => { + expect( + dynamicComponentsServiceStub.getUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + expect(reason).toEqual(new Error(assertionError)); + expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); + done(); + }); + }); + + it("should verify will reject when thrown error when creating ConfirmationContainerComponent", (done) => { + // Given + const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationContainerComponent`; + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ + uuid: CollectionsUtil.generateUUID(), + viewContainerRef: acquiredDynamicViewComponentRef, + hostView: acquiredDynamicHostViewStub, + }); + acquiredDynamicViewComponentRef.createComponent.and.throwError( + new Error("Error"), + ); - acquiredDynamicViewComponentRef = jasmine.createSpyObj('acquiredDynamicViewComponentRef', [ - 'createComponent', - 'clear' - ]); - acquiredDynamicHostViewStub = jasmine.createSpyObj('acquiredDynamicHostViewStub', ['destroy'], { - destroyed: false + // When/Then + service + .confirm({ + title: "Confirm title", + message: "Confirm message", + }) + .catch((reason) => { + expect( + dynamicComponentsServiceStub.getUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + expect( + acquiredDynamicViewComponentRef.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + containerChangeDetectorRefStub.detectChanges, + ).not.toHaveBeenCalled(); + expect(reason).toEqual(new Error(assertionError)); + expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); + done(); + }); + }); + + it("should verify will reject when thrown error when creating ConfirmationComponent", (done) => { + // Given + const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationComponent`; + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ + uuid: CollectionsUtil.generateUUID(), + viewContainerRef: acquiredDynamicViewComponentRef, + hostView: acquiredDynamicHostViewStub, }); + acquiredDynamicViewComponentRef.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.throwError( + new Error("Error"), + ); - containerChangeDetectorRefStub = jasmine.createSpyObj('confirmationContainerChangeDetectorRefStub', [ - 'detectChanges' - ]); - containerViewContainerRefStub = jasmine.createSpyObj( - 'confirmationContainerViewContainerRefStub', - ['createComponent', 'clear', 'indexOf', 'remove'], - { - length: 0 - } + // When/Then + service + .confirm({ + title: "Confirm title", + message: "Confirm message", + }) + .catch((reason) => { + expect( + dynamicComponentsServiceStub.getUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + expect( + acquiredDynamicViewComponentRef.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(1); + expect( + confirmationChangeDetectorRefStub.detectChanges, + ).not.toHaveBeenCalled(); + expect(reason).toEqual(new Error(assertionError)); + expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); + done(); + }); + }); + + it("should verify will reject when thrown error when creating component for message inside ConfirmationComponent", (done) => { + // Given + const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create Component instance for Confirmation Message`; + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ + uuid: CollectionsUtil.generateUUID(), + viewContainerRef: acquiredDynamicViewComponentRef, + hostView: acquiredDynamicHostViewStub, + }); + acquiredDynamicViewComponentRef.createComponent.and.returnValue( + containerComponentRefStub, ); - containerComponentRefStub = jasmine.createSpyObj>( - 'confirmationContainerComponentRefStub', - ['destroy'], - { - changeDetectorRef: containerChangeDetectorRefStub, - instance: { - viewContainerRef: containerViewContainerRefStub, - open: false - }, - hostView: { - destroyed: false - } as ViewRef - } + containerViewContainerRefStub.createComponent.and.returnValue( + confirmationComponentRefStub, + ); + containerViewContainerRefStub.indexOf.and.returnValue(0); + confirmationComponentViewContainerRefStub.createComponent.and.throwError( + new Error("Error"), ); - confirmationChangeDetectorRefStub = jasmine.createSpyObj('confirmationComponentChangeDetectorRefStub', [ - 'detectChanges' - ]); - confirmationComponentHostViewStub = jasmine.createSpyObj('confirmationComponentHostViewStub', ['destroy'], { - destroyed: false + // When/Then + service + .confirm({ + title: "Confirm title", + messageComponent: DummyMessageComponent, + }) + .catch((reason) => { + expect( + dynamicComponentsServiceStub.getUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + expect( + acquiredDynamicViewComponentRef.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(1); + expect( + confirmationComponentViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + confirmationChangeDetectorRefStub.detectChanges, + ).not.toHaveBeenCalled(); + expect(reason).toEqual(new Error(assertionError)); + expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); + done(); + }); + }); + + it("should verify will render message Component and resolve when handler is resolved", (done) => { + // Given + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ + uuid: CollectionsUtil.generateUUID(), + viewContainerRef: acquiredDynamicViewComponentRef, + hostView: acquiredDynamicHostViewStub, }); - confirmationComponentViewContainerRefStub = jasmine.createSpyObj('confirmationComponentViewContainerRefStub', [ - 'createComponent' - ]); - confirmationComponentRefStub = jasmine.createSpyObj>( - 'confirmationComponentRefStub', - ['destroy'], - { - changeDetectorRef: confirmationChangeDetectorRefStub, - instance: { - model: null, - doNotShowFutureConfirmation: false, - viewContainerRef: confirmationComponentViewContainerRefStub as ViewContainerRef - } as ConfirmationComponent, - hostView: confirmationComponentHostViewStub - } + acquiredDynamicViewComponentRef.createComponent.and.returnValue( + containerComponentRefStub, ); - confirmationMessageComponentRefStub = jasmine.createSpyObj>( - 'confirmationMessageComponentRefStub', - ['destroy'], - { - instance: { - messageCode: undefined - } as DummyMessageComponent - } + containerViewContainerRefStub.createComponent.and.returnValue( + confirmationComponentRefStub, + ); + containerViewContainerRefStub.indexOf.and.returnValue(0); + confirmationComponentViewContainerRefStub.createComponent.and.returnValue( + confirmationMessageComponentRefStub, ); - consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + // When/Then + service + .confirm({ + title: "some title", + messageComponent: DummyMessageComponent, + messageCode: "msg_code_100", + }) + .then((data) => { + verifyExpectationsInsidePromise(); + + expect(data).toEqual({ doNotShowFutureConfirmation: true }); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + done(); + }); + + // Then 1 + verifyExpectationsOutsidePromise(null, true); + + expect( + confirmationComponentViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect( + confirmationComponentViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DummyMessageComponent as any); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(2); + expect( + confirmationChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(1); + + expect(containerComponentRefStub.instance.open).toBeTrue(); + expect(confirmationComponentRefStub.instance.model).toBeInstanceOf( + ConfirmationModelImpl, + ); + expect(confirmationComponentRefStub.instance.model.title).toEqual( + "some title", + ); + expect( + confirmationComponentRefStub.instance.model.message, + ).toBeUndefined(); + expect( + confirmationComponentRefStub.instance.model.messageComponent, + ).toBeUndefined(); + expect( + confirmationComponentRefStub.instance.model.messageCode, + ).toBeUndefined(); + expect(confirmationComponentRefStub.instance.model.handler).toEqual({ + confirm: jasmine.any(Function), + dismiss: jasmine.any(Function), + }); - TestBed.configureTestingModule({ - declarations: [DummyMessageComponent], - providers: [{ provide: DynamicComponentsService, useValue: dynamicComponentsServiceStub }, ConfirmationService] + expect( + confirmationMessageComponentRefStub.instance.messageCode, + ).toEqual("msg_code_100"); + + confirmationComponentRefStub.instance.model.handler.confirm({ + doNotShowFutureConfirmation: true, }); + }); - service = TestBed.inject(ConfirmationService); - }); + it("should verify will resolve when handler is resolved and will clean resources after resolving", (done) => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); - it('should verify instance is created', () => { - // Then - expect(service).toBeDefined(); - expect(service).toBeInstanceOf(ConfirmationService); - expect(service).toBeInstanceOf(TaurusObject); - }); + // When/Then + service.confirm(inputModel).then((data) => { + verifyExpectationsInsidePromise(); - describe('Statics::', () => { - describe('Properties::', () => { - describe('|CLASS_NAME|', () => { - it('should verify the value', () => { - // Then - expect(ConfirmationService.CLASS_NAME).toEqual('ConfirmationService'); - }); - }); - }); - }); + expect(data).toEqual({ doNotShowFutureConfirmation: true }); - describe('Properties::', () => { - describe('|objectUUID|', () => { - it('should verify value is ConfirmationService', () => { - // Then - expect(/^ConfirmationService/.test(service.objectUUID)).toBeTrue(); - }); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + done(); }); - }); - describe('Methods::', () => { - const setupTestPrerequisites = () => { - const inputModel: ConfirmationInputModel = { title: 'Confirm title', message: 'Confirm message' }; - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ - uuid: CollectionsUtil.generateUUID(), - viewContainerRef: acquiredDynamicViewComponentRef, - hostView: acquiredDynamicHostViewStub - }); - acquiredDynamicViewComponentRef.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(confirmationComponentRefStub); - containerViewContainerRefStub.indexOf.and.returnValue(0); - - return inputModel; - }; + // Then 1 + verifyExpectationsOutsidePromise(inputModel); - describe('|initialize|', () => { - it(`should verify invoking won't throw error`, () => { - // When/Then - expect(() => service.initialize()).not.toThrowError(); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); + confirmationComponentRefStub.instance.model.handler.confirm({ + doNotShowFutureConfirmation: true, }); + }); - describe('|confirm|', () => { - const verifyExpectationsInsidePromise = () => { - expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith(confirmationComponentHostViewStub); - expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); - - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(3); - - expect(confirmationComponentRefStub.destroy).toHaveBeenCalledTimes(1); - - expect(containerComponentRefStub.instance.open).toBeFalse(); - }; - const verifyExpectationsOutsidePromise = (inputModel: ConfirmationInputModel, verifyCreationOnly = false) => { - expect(dynamicComponentsServiceStub.getUniqueViewContainerRef).toHaveBeenCalledTimes(1); - - expect(acquiredDynamicViewComponentRef.createComponent).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(acquiredDynamicViewComponentRef.createComponent).toHaveBeenCalledWith(ConfirmationContainerComponent as any); - - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledWith(ConfirmationComponent as any); - - if (verifyCreationOnly) { - return; - } - - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(2); - expect(confirmationChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(1); - - expect(containerComponentRefStub.instance.open).toBeTrue(); - expect(confirmationComponentRefStub.instance.model).toBeInstanceOf(ConfirmationModelImpl); - expect(confirmationComponentRefStub.instance.model.title).toEqual(inputModel.title); - expect(confirmationComponentRefStub.instance.model.message).toEqual(inputModel.message); - expect(confirmationComponentRefStub.instance.model.messageComponent).toBeUndefined(); - expect(confirmationComponentRefStub.instance.model.messageCode).toBeUndefined(); - expect(confirmationComponentRefStub.instance.model.handler).toEqual({ - confirm: jasmine.any(Function), - dismiss: jasmine.any(Function) - }); - }; - - it('should verify will reject when cannot acquired ViewContainerRef from DynamicComponentsService', (done) => { - // Given - const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, cannot acquire unique ViewContainerRef where to insert confirmation Views`; - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue(null); - - // When/Then - service - .confirm({ - title: 'Confirm title', - message: 'Confirm message' - }) - .catch((reason) => { - expect(dynamicComponentsServiceStub.getUniqueViewContainerRef).toHaveBeenCalledTimes(1); - expect(reason).toEqual(new Error(assertionError)); - expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); - done(); - }); - }); - - it('should verify will reject when thrown error when creating ConfirmationContainerComponent', (done) => { - // Given - const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationContainerComponent`; - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ - uuid: CollectionsUtil.generateUUID(), - viewContainerRef: acquiredDynamicViewComponentRef, - hostView: acquiredDynamicHostViewStub - }); - acquiredDynamicViewComponentRef.createComponent.and.throwError(new Error('Error')); - - // When/Then - service - .confirm({ - title: 'Confirm title', - message: 'Confirm message' - }) - .catch((reason) => { - expect(dynamicComponentsServiceStub.getUniqueViewContainerRef).toHaveBeenCalledTimes(1); - expect(acquiredDynamicViewComponentRef.createComponent).toHaveBeenCalledTimes(1); - expect(containerChangeDetectorRefStub.detectChanges).not.toHaveBeenCalled(); - expect(reason).toEqual(new Error(assertionError)); - expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); - done(); - }); - }); - - it('should verify will reject when thrown error when creating ConfirmationComponent', (done) => { - // Given - const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationComponent`; - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ - uuid: CollectionsUtil.generateUUID(), - viewContainerRef: acquiredDynamicViewComponentRef, - hostView: acquiredDynamicHostViewStub - }); - acquiredDynamicViewComponentRef.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.throwError(new Error('Error')); - - // When/Then - service - .confirm({ - title: 'Confirm title', - message: 'Confirm message' - }) - .catch((reason) => { - expect(dynamicComponentsServiceStub.getUniqueViewContainerRef).toHaveBeenCalledTimes(1); - expect(acquiredDynamicViewComponentRef.createComponent).toHaveBeenCalledTimes(1); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledTimes(1); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(1); - expect(confirmationChangeDetectorRefStub.detectChanges).not.toHaveBeenCalled(); - expect(reason).toEqual(new Error(assertionError)); - expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); - done(); - }); - }); - - it('should verify will reject when thrown error when creating component for message inside ConfirmationComponent', (done) => { - // Given - const assertionError = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create Component instance for Confirmation Message`; - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ - uuid: CollectionsUtil.generateUUID(), - viewContainerRef: acquiredDynamicViewComponentRef, - hostView: acquiredDynamicHostViewStub - }); - acquiredDynamicViewComponentRef.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(confirmationComponentRefStub); - containerViewContainerRefStub.indexOf.and.returnValue(0); - confirmationComponentViewContainerRefStub.createComponent.and.throwError(new Error('Error')); - - // When/Then - service - .confirm({ - title: 'Confirm title', - messageComponent: DummyMessageComponent - }) - .catch((reason) => { - expect(dynamicComponentsServiceStub.getUniqueViewContainerRef).toHaveBeenCalledTimes(1); - expect(acquiredDynamicViewComponentRef.createComponent).toHaveBeenCalledTimes(1); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledTimes(1); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(1); - expect(confirmationComponentViewContainerRefStub.createComponent).toHaveBeenCalledTimes(1); - expect(confirmationChangeDetectorRefStub.detectChanges).not.toHaveBeenCalled(); - expect(reason).toEqual(new Error(assertionError)); - expect(consoleErrorSpy).toHaveBeenCalledWith(assertionError); - done(); - }); - }); - - it('should verify will render message Component and resolve when handler is resolved', (done) => { - // Given - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ - uuid: CollectionsUtil.generateUUID(), - viewContainerRef: acquiredDynamicViewComponentRef, - hostView: acquiredDynamicHostViewStub - }); - acquiredDynamicViewComponentRef.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(confirmationComponentRefStub); - containerViewContainerRefStub.indexOf.and.returnValue(0); - confirmationComponentViewContainerRefStub.createComponent.and.returnValue(confirmationMessageComponentRefStub); - - // When/Then - service - .confirm({ - title: 'some title', - messageComponent: DummyMessageComponent, - messageCode: 'msg_code_100' - }) - .then((data) => { - verifyExpectationsInsidePromise(); - - expect(data).toEqual({ doNotShowFutureConfirmation: true }); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - - done(); - }); - - // Then 1 - verifyExpectationsOutsidePromise(null, true); - - expect(confirmationComponentViewContainerRefStub.createComponent).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(confirmationComponentViewContainerRefStub.createComponent).toHaveBeenCalledWith(DummyMessageComponent as any); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(2); - expect(confirmationChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(1); - - expect(containerComponentRefStub.instance.open).toBeTrue(); - expect(confirmationComponentRefStub.instance.model).toBeInstanceOf(ConfirmationModelImpl); - expect(confirmationComponentRefStub.instance.model.title).toEqual('some title'); - expect(confirmationComponentRefStub.instance.model.message).toBeUndefined(); - expect(confirmationComponentRefStub.instance.model.messageComponent).toBeUndefined(); - expect(confirmationComponentRefStub.instance.model.messageCode).toBeUndefined(); - expect(confirmationComponentRefStub.instance.model.handler).toEqual({ - confirm: jasmine.any(Function), - dismiss: jasmine.any(Function) - }); - - expect(confirmationMessageComponentRefStub.instance.messageCode).toEqual('msg_code_100'); - - confirmationComponentRefStub.instance.model.handler.confirm({ doNotShowFutureConfirmation: true }); - }); - - it('should verify will resolve when handler is resolved and will clean resources after resolving', (done) => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); - - // When/Then - service.confirm(inputModel).then((data) => { - verifyExpectationsInsidePromise(); - - expect(data).toEqual({ doNotShowFutureConfirmation: true }); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - done(); - }); - - // Then 1 - verifyExpectationsOutsidePromise(inputModel); - - confirmationComponentRefStub.instance.model.handler.confirm({ doNotShowFutureConfirmation: true }); - }); - - it('should verify will resolve when handler is resolved and will fail to clean resources after resolving', (done) => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); - containerViewContainerRefStub.remove.and.throwError(new Error('Error')); - - // When/Then - service.confirm(inputModel).then((data) => { - expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith(confirmationComponentHostViewStub); - expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); - - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(3); - - expect(confirmationComponentRefStub.destroy).not.toHaveBeenCalled(); - - expect(containerComponentRefStub.instance.open).toBeFalse(); - - expect(data).toEqual({ doNotShowFutureConfirmation: false }); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to cleanup confirmation views after User action` - ); - done(); - }); - - // Then 1 - verifyExpectationsOutsidePromise(inputModel); - - confirmationComponentRefStub.instance.model.handler.confirm({ doNotShowFutureConfirmation: false }); - }); - - it('should verify will resolve when handler is resolved and will fail to clean resources with total damage for UX', (done) => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); - let counter = 0; - containerChangeDetectorRefStub.detectChanges.and.callFake(() => { - if (++counter >= 3) { - throw new Error('Error'); - } - }); + it("should verify will resolve when handler is resolved and will fail to clean resources after resolving", (done) => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + containerViewContainerRefStub.remove.and.throwError(new Error("Error")); - // When/Then - service.confirm(inputModel).then((data) => { - expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith(confirmationComponentHostViewStub); - expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); + // When/Then + service.confirm(inputModel).then((data) => { + expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith( + confirmationComponentHostViewStub, + ); + expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(4); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(3); - expect(confirmationComponentRefStub.destroy).toHaveBeenCalled(); + expect(confirmationComponentRefStub.destroy).not.toHaveBeenCalled(); - expect(containerComponentRefStub.instance.open).toBeFalse(); + expect(containerComponentRefStub.instance.open).toBeFalse(); - expect(data).toEqual({ doNotShowFutureConfirmation: false }); + expect(data).toEqual({ doNotShowFutureConfirmation: false }); - expect(consoleErrorSpy.calls.argsFor(0)).toEqual([ - `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to cleanup confirmation views after User action` - ]); - expect(consoleErrorSpy.calls.argsFor(1)).toEqual([ - `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to force container to hide` - ]); - done(); - }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to cleanup confirmation views after User action`, + ); + done(); + }); - // Then 1 - verifyExpectationsOutsidePromise(inputModel); + // Then 1 + verifyExpectationsOutsidePromise(inputModel); - confirmationComponentRefStub.instance.model.handler.confirm({ doNotShowFutureConfirmation: false }); - }); + confirmationComponentRefStub.instance.model.handler.confirm({ + doNotShowFutureConfirmation: false, + }); + }); + + it("should verify will resolve when handler is resolved and will fail to clean resources with total damage for UX", (done) => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + let counter = 0; + containerChangeDetectorRefStub.detectChanges.and.callFake(() => { + if (++counter >= 3) { + throw new Error("Error"); + } + }); - it('should verify will resolve when handler is resolved and on second invoke will acquire and create new reference because of unhealthy state', fakeAsync(() => { - // Given - const uuid = CollectionsUtil.generateUUID(); - const inputModel: ConfirmationInputModel = { title: 'Confirm title 50', message: 'Confirm message 50' }; - dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ - uuid, - viewContainerRef: acquiredDynamicViewComponentRef, - hostView: jasmine.createSpyObj('acquiredDynamicHostViewStub', ['destroy'], { - destroyed: true - }) - }); - acquiredDynamicViewComponentRef.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(confirmationComponentRefStub); - containerViewContainerRefStub.indexOf.and.returnValue(0); + // When/Then + service.confirm(inputModel).then((data) => { + expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith( + confirmationComponentHostViewStub, + ); + expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); - // When/Then 1 - service.confirm(inputModel).then((data) => { - expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith(confirmationComponentHostViewStub); - expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(4); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(3); + expect(confirmationComponentRefStub.destroy).toHaveBeenCalled(); - expect(confirmationComponentRefStub.destroy).toHaveBeenCalled(); + expect(containerComponentRefStub.instance.open).toBeFalse(); - expect(containerComponentRefStub.instance.open).toBeFalse(); + expect(data).toEqual({ doNotShowFutureConfirmation: false }); - expect(data).toEqual({ doNotShowFutureConfirmation: false }); - }); + expect(consoleErrorSpy.calls.argsFor(0)).toEqual([ + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to cleanup confirmation views after User action`, + ]); + expect(consoleErrorSpy.calls.argsFor(1)).toEqual([ + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to force container to hide`, + ]); + done(); + }); - // Then 1 - verifyExpectationsOutsidePromise(inputModel); + // Then 1 + verifyExpectationsOutsidePromise(inputModel); - confirmationComponentRefStub.instance.model.handler.confirm({ doNotShowFutureConfirmation: false }); + confirmationComponentRefStub.instance.model.handler.confirm({ + doNotShowFutureConfirmation: false, + }); + }); + + it("should verify will resolve when handler is resolved and on second invoke will acquire and create new reference because of unhealthy state", fakeAsync(() => { + // Given + const uuid = CollectionsUtil.generateUUID(); + const inputModel: ConfirmationInputModel = { + title: "Confirm title 50", + message: "Confirm message 50", + }; + dynamicComponentsServiceStub.getUniqueViewContainerRef.and.returnValue({ + uuid, + viewContainerRef: acquiredDynamicViewComponentRef, + hostView: jasmine.createSpyObj( + "acquiredDynamicHostViewStub", + ["destroy"], + { + destroyed: true, + }, + ), + }); + acquiredDynamicViewComponentRef.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + confirmationComponentRefStub, + ); + containerViewContainerRefStub.indexOf.and.returnValue(0); - tick(1000); + // When/Then 1 + service.confirm(inputModel).then((data) => { + expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith( + confirmationComponentHostViewStub, + ); + expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); - // When/Then 2 - service.confirm(inputModel).then((data) => { - expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith(confirmationComponentHostViewStub); - expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(3); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(6); + expect(confirmationComponentRefStub.destroy).toHaveBeenCalled(); - expect(confirmationComponentRefStub.destroy).toHaveBeenCalled(); + expect(containerComponentRefStub.instance.open).toBeFalse(); - expect(containerComponentRefStub.instance.open).toBeFalse(); + expect(data).toEqual({ doNotShowFutureConfirmation: false }); + }); - expect(data).toEqual({ doNotShowFutureConfirmation: true }); - }); + // Then 1 + verifyExpectationsOutsidePromise(inputModel); - // Then 2 - // clean resources - expect(confirmationComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(acquiredDynamicViewComponentRef.clear).toHaveBeenCalledTimes(1); - expect(dynamicComponentsServiceStub.destroyUniqueViewContainerRef).toHaveBeenCalledTimes(1); - expect(dynamicComponentsServiceStub.destroyUniqueViewContainerRef).toHaveBeenCalledWith(uuid); - - expect(dynamicComponentsServiceStub.getUniqueViewContainerRef).toHaveBeenCalledTimes(2); - expect(acquiredDynamicViewComponentRef.createComponent).toHaveBeenCalledTimes(2); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledTimes(2); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(5); - expect(confirmationChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(2); - - expect(containerComponentRefStub.instance.open).toBeTrue(); - expect(confirmationComponentRefStub.instance.model).toBeInstanceOf(ConfirmationModelImpl); + confirmationComponentRefStub.instance.model.handler.confirm({ + doNotShowFutureConfirmation: false, + }); - confirmationComponentRefStub.instance.model.handler.confirm({ doNotShowFutureConfirmation: true }); + tick(1000); - tick(1000); - })); + // When/Then 2 + service.confirm(inputModel).then((data) => { + expect(containerViewContainerRefStub.indexOf).toHaveBeenCalledWith( + confirmationComponentHostViewStub, + ); + expect(containerViewContainerRefStub.remove).toHaveBeenCalledWith(0); - it('should verify will reject when handler is rejected and will clean resources after resolving', (done) => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(6); - // When/Then - service - .confirm(inputModel) - .then(() => { - done.fail(`Should not enter here`); - }) - .catch((reason) => { - verifyExpectationsInsidePromise(); - - expect(reason).toEqual('Rejecting on user behalf'); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - done(); - }); + expect(confirmationComponentRefStub.destroy).toHaveBeenCalled(); - // Then 1 - verifyExpectationsOutsidePromise(inputModel); + expect(containerComponentRefStub.instance.open).toBeFalse(); - confirmationComponentRefStub.instance.model.handler.dismiss('Rejecting on user behalf'); - }); + expect(data).toEqual({ doNotShowFutureConfirmation: true }); + }); - it('should verify will reject when handler is rejected and will clean resources after resolving and log error because forcefully destroyed', (done) => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + // Then 2 + // clean resources + expect(confirmationComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(acquiredDynamicViewComponentRef.clear).toHaveBeenCalledTimes(1); + expect( + dynamicComponentsServiceStub.destroyUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + expect( + dynamicComponentsServiceStub.destroyUniqueViewContainerRef, + ).toHaveBeenCalledWith(uuid); + + expect( + dynamicComponentsServiceStub.getUniqueViewContainerRef, + ).toHaveBeenCalledTimes(2); + expect( + acquiredDynamicViewComponentRef.createComponent, + ).toHaveBeenCalledTimes(2); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(2); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(5); + expect( + confirmationChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(2); + + expect(containerComponentRefStub.instance.open).toBeTrue(); + expect(confirmationComponentRefStub.instance.model).toBeInstanceOf( + ConfirmationModelImpl, + ); - // When/Then - service - .confirm(inputModel) - .then(() => { - done.fail(`Should not enter here`); - }) - .catch((reason) => { - verifyExpectationsInsidePromise(); + confirmationComponentRefStub.instance.model.handler.confirm({ + doNotShowFutureConfirmation: true, + }); + + tick(1000); + })); + + it("should verify will reject when handler is rejected and will clean resources after resolving", (done) => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + + // When/Then + service + .confirm(inputModel) + .then(() => { + done.fail(`Should not enter here`); + }) + .catch((reason) => { + verifyExpectationsInsidePromise(); + + expect(reason).toEqual("Rejecting on user behalf"); - expect(reason).toEqual(new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT)); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + done(); + }); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${ConfirmationService.CLASS_NAME}: Potential bug found, views where destroyed externally from unknown source` - ); - done(); - }); + // Then 1 + verifyExpectationsOutsidePromise(inputModel); - // Then 1 - verifyExpectationsOutsidePromise(inputModel); + confirmationComponentRefStub.instance.model.handler.dismiss( + "Rejecting on user behalf", + ); + }); + + it("should verify will reject when handler is rejected and will clean resources after resolving and log error because forcefully destroyed", (done) => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + + // When/Then + service + .confirm(inputModel) + .then(() => { + done.fail(`Should not enter here`); + }) + .catch((reason) => { + verifyExpectationsInsidePromise(); + + expect(reason).toEqual( + new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT), + ); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${ConfirmationService.CLASS_NAME}: Potential bug found, views where destroyed externally from unknown source`, + ); + done(); + }); + + // Then 1 + verifyExpectationsOutsidePromise(inputModel); + + confirmationComponentRefStub.instance.model.handler.dismiss( + new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT), + ); + }); + }); - confirmationComponentRefStub.instance.model.handler.dismiss( - new Error(ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT) - ); - }); + describe("|ngOnDestroy|", () => { + it("should verify will try to clean resources, there is empty state", () => { + // When + service.ngOnDestroy(); + + // Then + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(confirmationComponentRefStub.destroy).not.toHaveBeenCalled(); + expect(acquiredDynamicViewComponentRef.clear).not.toHaveBeenCalled(); + expect( + dynamicComponentsServiceStub.destroyUniqueViewContainerRef, + ).not.toHaveBeenCalled(); + }); + + it("should verify will clean resources, there is state", () => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + + // When/Then + service.confirm(inputModel).then(() => { + // No-op. }); - describe('|ngOnDestroy|', () => { - it('should verify will try to clean resources, there is empty state', () => { - // When - service.ngOnDestroy(); - - // Then - expect(consoleErrorSpy).not.toHaveBeenCalled(); - expect(confirmationComponentRefStub.destroy).not.toHaveBeenCalled(); - expect(acquiredDynamicViewComponentRef.clear).not.toHaveBeenCalled(); - expect(dynamicComponentsServiceStub.destroyUniqueViewContainerRef).not.toHaveBeenCalled(); - }); - - it('should verify will clean resources, there is state', () => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); - - // When/Then - service.confirm(inputModel).then(() => { - // No-op. - }); - - // When - service.ngOnDestroy(); - - // Then - expect(consoleErrorSpy).not.toHaveBeenCalled(); - expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(acquiredDynamicViewComponentRef.clear).toHaveBeenCalledTimes(1); - expect(dynamicComponentsServiceStub.destroyUniqueViewContainerRef).toHaveBeenCalledTimes(1); - }); - - it('should verify will try to clean resources, but error would be thrown', () => { - // Given - const inputModel: ConfirmationInputModel = setupTestPrerequisites(); - - containerComponentRefStub.destroy.and.throwError(new Error('Error')); - acquiredDynamicViewComponentRef.clear.and.throwError(new Error('Error')); - - // When/Then - service.confirm(inputModel).then(() => { - // No-op. - }); - - // When - service.ngOnDestroy(); - - // Then - expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(acquiredDynamicViewComponentRef.clear).toHaveBeenCalledTimes(1); - expect(dynamicComponentsServiceStub.destroyUniqueViewContainerRef).toHaveBeenCalledTimes(1); - expect(consoleErrorSpy.calls.argsFor(0)).toEqual([ - `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to destroy ConfirmationContainerComponent reference` - ]); - expect(consoleErrorSpy.calls.argsFor(1)).toEqual([ - `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to clear views in acquired ViewContainerRef` - ]); - }); + // When + service.ngOnDestroy(); + + // Then + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(acquiredDynamicViewComponentRef.clear).toHaveBeenCalledTimes(1); + expect( + dynamicComponentsServiceStub.destroyUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + }); + + it("should verify will try to clean resources, but error would be thrown", () => { + // Given + const inputModel: ConfirmationInputModel = setupTestPrerequisites(); + + containerComponentRefStub.destroy.and.throwError(new Error("Error")); + acquiredDynamicViewComponentRef.clear.and.throwError( + new Error("Error"), + ); + + // When/Then + service.confirm(inputModel).then(() => { + // No-op. }); + + // When + service.ngOnDestroy(); + + // Then + expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(acquiredDynamicViewComponentRef.clear).toHaveBeenCalledTimes(1); + expect( + dynamicComponentsServiceStub.destroyUniqueViewContainerRef, + ).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy.calls.argsFor(0)).toEqual([ + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to destroy ConfirmationContainerComponent reference`, + ]); + expect(consoleErrorSpy.calls.argsFor(1)).toEqual([ + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to clear views in acquired ViewContainerRef`, + ]); + }); }); + }); }); @Component({ - selector: 'shared-dummy-message-component', - template: `

    some text

    ` + selector: "shared-dummy-message-component", + template: `

    some text

    `, }) class DummyMessageComponent { - @Input() messageCode: string; + @Input() messageCode: string; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.ts index d24a90035a..76cbf766ff 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/confirmation.service.ts @@ -3,18 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentRef, Injectable, OnDestroy, ViewContainerRef, ViewRef } from '@angular/core'; +import { + ComponentRef, + Injectable, + OnDestroy, + ViewContainerRef, + ViewRef, +} from "@angular/core"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { DynamicComponentsService } from '../../dynamic-components'; +import { DynamicComponentsService } from "../../dynamic-components"; -import { ConfirmationInputModel, ConfirmationOutputModel } from '../model'; -import { ConfirmationModelImpl, ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT } from '../model/confirmation.model'; +import { ConfirmationInputModel, ConfirmationOutputModel } from "../model"; +import { + ConfirmationModelImpl, + ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT, +} from "../model/confirmation.model"; -import { ConfirmationComponent, ConfirmationContainerComponent } from '../components'; +import { + ConfirmationComponent, + ConfirmationContainerComponent, +} from "../components"; /** * ** Confirmation Service that create confirmation view for every confirm request, @@ -24,289 +36,347 @@ import { ConfirmationComponent, ConfirmationContainerComponent } from '../compon */ @Injectable() export class ConfirmationService extends TaurusObject implements OnDestroy { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'ConfirmationService'; - - /** - * ** Acquired ViewContainerRef from {@link DynamicComponentsService}, - * where Confirmation Container {@link ConfirmationContainerComponent} will be inserted. - * @private - */ - private _acquiredViewContainerRef: { uuid: string; viewContainerRef: ViewContainerRef; hostView: ViewRef }; - - /** - * ** Confirmation Container reference {@link ConfirmationContainerComponent}, - * where all contextual Confirmation components {@link ConfirmationComponent} will be inserted. - * @private - */ - private _confirmationContainerRef: ComponentRef; - - /** - * ** Constructor. - */ - constructor(private readonly dynamicComponentsService: DynamicComponentsService) { - super(ConfirmationService.CLASS_NAME); + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "ConfirmationService"; + + /** + * ** Acquired ViewContainerRef from {@link DynamicComponentsService}, + * where Confirmation Container {@link ConfirmationContainerComponent} will be inserted. + * @private + */ + private _acquiredViewContainerRef: { + uuid: string; + viewContainerRef: ViewContainerRef; + hostView: ViewRef; + }; + + /** + * ** Confirmation Container reference {@link ConfirmationContainerComponent}, + * where all contextual Confirmation components {@link ConfirmationComponent} will be inserted. + * @private + */ + private _confirmationContainerRef: ComponentRef; + + /** + * ** Constructor. + */ + constructor( + private readonly dynamicComponentsService: DynamicComponentsService, + ) { + super(ConfirmationService.CLASS_NAME); + } + + /** + * ** Show confirm view according the provided model instructions, and return flow to the invoker upon User action Confirm/Reject. + * + * - Utilizes Promise for communication. + * - Sets some default values if model instructions are incomplete because most of them are optional. + */ + confirm(model: ConfirmationInputModel): Promise { + const modelImpl = new ConfirmationModelImpl(model); + const promise = new Promise((resolve, reject) => { + modelImpl.handler.confirm = resolve; + modelImpl.handler.dismiss = reject; + }); + + const acquireViewContainerRefStatus = + this._acquireDynamicViewContainerRef(); + if (!acquireViewContainerRefStatus.status) { + return Promise.reject(new Error(acquireViewContainerRefStatus.error)); } - /** - * ** Show confirm view according the provided model instructions, and return flow to the invoker upon User action Confirm/Reject. - * - * - Utilizes Promise for communication. - * - Sets some default values if model instructions are incomplete because most of them are optional. - */ - confirm(model: ConfirmationInputModel): Promise { - const modelImpl = new ConfirmationModelImpl(model); - const promise = new Promise((resolve, reject) => { - modelImpl.handler.confirm = resolve; - modelImpl.handler.dismiss = reject; - }); - - const acquireViewContainerRefStatus = this._acquireDynamicViewContainerRef(); - if (!acquireViewContainerRefStatus.status) { - return Promise.reject(new Error(acquireViewContainerRefStatus.error)); - } - - const createConfContainerComponentStatus = this._createConfirmationContainerComponent(); - if (!createConfContainerComponentStatus.status) { - return Promise.reject(new Error(createConfContainerComponentStatus.error)); - } - - const createConfComponentStatus = this._createConfirmationComponent(modelImpl); - if (!createConfComponentStatus.status) { - return Promise.reject(new Error(createConfComponentStatus.error)); - } - - if (!this._confirmationContainerRef.instance.open) { - this._confirmationContainerRef.instance.open = true; - this._confirmationContainerRef.changeDetectorRef.detectChanges(); - } - - return promise - .catch((reason: ConfirmationOutputModel) => { - if (reason instanceof Error && reason.message === ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT) { - console.error( - `${ConfirmationService.CLASS_NAME}: Potential bug found, views where destroyed externally from unknown source` - ); - } - - throw reason; - }) - .finally(() => { - this._clearSingleConfirmationResources(createConfComponentStatus as { componentRef: ComponentRef }); - }); + const createConfContainerComponentStatus = + this._createConfirmationContainerComponent(); + if (!createConfContainerComponentStatus.status) { + return Promise.reject( + new Error(createConfContainerComponentStatus.error), + ); } - /** - * ** Initialize service. - * - * - Should be invoked only once. - * - Ideal place for invoking is AppComponent.ngOnInit(). - */ - initialize(): void { - // No-op. + const createConfComponentStatus = + this._createConfirmationComponent(modelImpl); + if (!createConfComponentStatus.status) { + return Promise.reject(new Error(createConfComponentStatus.error)); } - /** - * @inheritDoc - */ - override ngOnDestroy(): void { - this._clearResources(); - - super.ngOnDestroy(); + if (!this._confirmationContainerRef.instance.open) { + this._confirmationContainerRef.instance.open = true; + this._confirmationContainerRef.changeDetectorRef.detectChanges(); } - private _acquireDynamicViewContainerRef(): { status: boolean; error?: string } { - if (this._isAcquiredViewContainerRefUnhealthy()) { - this._clearResources(); + return promise + .catch((reason: ConfirmationOutputModel) => { + if ( + reason instanceof Error && + reason.message === + ERROR_CODE_CONFIRMATION_FORCEFULLY_DESTROYED_COMPONENT + ) { + console.error( + `${ConfirmationService.CLASS_NAME}: Potential bug found, views where destroyed externally from unknown source`, + ); } - if (!this._acquiredViewContainerRef) { - const acquiredViewContainerRef = this.dynamicComponentsService.getUniqueViewContainerRef(); - if (!acquiredViewContainerRef) { - const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, cannot acquire unique ViewContainerRef where to insert confirmation Views`; - console.error(errorMessage); - - return { - status: false, - error: errorMessage - }; - } + throw reason; + }) + .finally(() => { + this._clearSingleConfirmationResources( + createConfComponentStatus as { + componentRef: ComponentRef; + }, + ); + }); + } + + /** + * ** Initialize service. + * + * - Should be invoked only once. + * - Ideal place for invoking is AppComponent.ngOnInit(). + */ + initialize(): void { + // No-op. + } + + /** + * @inheritDoc + */ + override ngOnDestroy(): void { + this._clearResources(); + + super.ngOnDestroy(); + } + + private _acquireDynamicViewContainerRef(): { + status: boolean; + error?: string; + } { + if (this._isAcquiredViewContainerRefUnhealthy()) { + this._clearResources(); + } - this._acquiredViewContainerRef = acquiredViewContainerRef; - } + if (!this._acquiredViewContainerRef) { + const acquiredViewContainerRef = + this.dynamicComponentsService.getUniqueViewContainerRef(); + if (!acquiredViewContainerRef) { + const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, cannot acquire unique ViewContainerRef where to insert confirmation Views`; + console.error(errorMessage); return { - status: true + status: false, + error: errorMessage, }; + } + + this._acquiredViewContainerRef = acquiredViewContainerRef; } - private _createConfirmationContainerComponent(): { status: boolean; error?: string } { - if (!this._confirmationContainerRef) { - try { - this._confirmationContainerRef = - this._acquiredViewContainerRef.viewContainerRef.createComponent(ConfirmationContainerComponent); - this._confirmationContainerRef.changeDetectorRef.detectChanges(); - } catch (e) { - const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationContainerComponent`; - console.error(errorMessage); - - return { - status: false, - error: errorMessage - }; - } - } + return { + status: true, + }; + } + + private _createConfirmationContainerComponent(): { + status: boolean; + error?: string; + } { + if (!this._confirmationContainerRef) { + try { + this._confirmationContainerRef = + this._acquiredViewContainerRef.viewContainerRef.createComponent( + ConfirmationContainerComponent, + ); + this._confirmationContainerRef.changeDetectorRef.detectChanges(); + } catch (e) { + const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationContainerComponent`; + console.error(errorMessage); return { - status: true + status: false, + error: errorMessage, }; + } } - private _createConfirmationComponent(model: ConfirmationModelImpl): { - status: boolean; - componentRef?: ComponentRef; - error?: string; - } { - try { - const confirmationComponentRef = - this._confirmationContainerRef.instance.viewContainerRef.createComponent(ConfirmationComponent); - - const assignMessageAndModelStatus = ConfirmationService._assignMessageAndModel(confirmationComponentRef, model); - - if (!assignMessageAndModelStatus.status) { - console.error(assignMessageAndModelStatus.error); - - return { - status: false, - error: assignMessageAndModelStatus.error - }; - } - - confirmationComponentRef.changeDetectorRef.detectChanges(); - - return { - status: true, - componentRef: confirmationComponentRef - }; - } catch (e) { - const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationComponent`; - console.error(errorMessage); - - return { - status: false, - error: errorMessage - }; - } - } + return { + status: true, + }; + } + + private _createConfirmationComponent(model: ConfirmationModelImpl): { + status: boolean; + componentRef?: ComponentRef; + error?: string; + } { + try { + const confirmationComponentRef = + this._confirmationContainerRef.instance.viewContainerRef.createComponent( + ConfirmationComponent, + ); - private _isAcquiredViewContainerRefUnhealthy(): boolean { - return ( - this._acquiredViewContainerRef && this._acquiredViewContainerRef.hostView && this._acquiredViewContainerRef.hostView.destroyed + const assignMessageAndModelStatus = + ConfirmationService._assignMessageAndModel( + confirmationComponentRef, + model, ); - } - private _refineConfirmationContainerVisibility(forceHide = false): void { - if (this._confirmationContainerRef.instance.viewContainerRef.length === 0 || forceHide) { - this._confirmationContainerRef.instance.open = false; - this._confirmationContainerRef.changeDetectorRef.detectChanges(); - } + if (!assignMessageAndModelStatus.status) { + console.error(assignMessageAndModelStatus.error); + + return { + status: false, + error: assignMessageAndModelStatus.error, + }; + } + + confirmationComponentRef.changeDetectorRef.detectChanges(); + + return { + status: true, + componentRef: confirmationComponentRef, + }; + } catch (e) { + const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create instance of ConfirmationComponent`; + console.error(errorMessage); + + return { + status: false, + error: errorMessage, + }; } + } + + private _isAcquiredViewContainerRefUnhealthy(): boolean { + return ( + this._acquiredViewContainerRef && + this._acquiredViewContainerRef.hostView && + this._acquiredViewContainerRef.hostView.destroyed + ); + } + + private _refineConfirmationContainerVisibility(forceHide = false): void { + if ( + this._confirmationContainerRef.instance.viewContainerRef.length === 0 || + forceHide + ) { + this._confirmationContainerRef.instance.open = false; + this._confirmationContainerRef.changeDetectorRef.detectChanges(); + } + } + + private _clearSingleConfirmationResources(internalModelRef: { + componentRef: ComponentRef; + }): void { + try { + const foundViewRefIndex = + this._confirmationContainerRef.instance.viewContainerRef.indexOf( + internalModelRef.componentRef.hostView, + ); + if (foundViewRefIndex !== -1) { + this._confirmationContainerRef.instance.viewContainerRef.remove( + foundViewRefIndex, + ); + } - private _clearSingleConfirmationResources(internalModelRef: { componentRef: ComponentRef }): void { - try { - const foundViewRefIndex = this._confirmationContainerRef.instance.viewContainerRef.indexOf( - internalModelRef.componentRef.hostView - ); - if (foundViewRefIndex !== -1) { - this._confirmationContainerRef.instance.viewContainerRef.remove(foundViewRefIndex); - } + if (!internalModelRef.componentRef.hostView.destroyed) { + internalModelRef.componentRef.destroy(); + } - if (!internalModelRef.componentRef.hostView.destroyed) { - internalModelRef.componentRef.destroy(); - } + internalModelRef.componentRef = null; - internalModelRef.componentRef = null; + this._refineConfirmationContainerVisibility(); - this._refineConfirmationContainerVisibility(); + return; + } catch (e) { + console.error( + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to cleanup confirmation views after User action`, + ); + } - return; - } catch (e) { - console.error(`${ConfirmationService.CLASS_NAME}: Potential bug found, failed to cleanup confirmation views after User action`); - } + try { + this._refineConfirmationContainerVisibility(true); + } catch (e) { + console.error( + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to force container to hide`, + ); + } + } + + private _clearResources(): void { + try { + if ( + this._confirmationContainerRef?.hostView && + !this._confirmationContainerRef.hostView.destroyed + ) { + this._confirmationContainerRef.destroy(); + } + } catch (e) { + console.error( + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to destroy ConfirmationContainerComponent reference`, + ); + } - try { - this._refineConfirmationContainerVisibility(true); - } catch (e) { - console.error(`${ConfirmationService.CLASS_NAME}: Potential bug found, failed to force container to hide`); - } + this._confirmationContainerRef = null; + + if (!this._acquiredViewContainerRef) { + return; } - private _clearResources(): void { - try { - if (this._confirmationContainerRef?.hostView && !this._confirmationContainerRef.hostView.destroyed) { - this._confirmationContainerRef.destroy(); - } - } catch (e) { - console.error( - `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to destroy ConfirmationContainerComponent reference` - ); - } + try { + this._acquiredViewContainerRef.viewContainerRef.clear(); + } catch (e) { + console.error( + `${ConfirmationService.CLASS_NAME}: Potential bug found, failed to clear views in acquired ViewContainerRef`, + ); + } - this._confirmationContainerRef = null; + this.dynamicComponentsService.destroyUniqueViewContainerRef( + this._acquiredViewContainerRef.uuid, + ); - if (!this._acquiredViewContainerRef) { - return; - } + this._acquiredViewContainerRef = null; + } - try { - this._acquiredViewContainerRef.viewContainerRef.clear(); - } catch (e) { - console.error(`${ConfirmationService.CLASS_NAME}: Potential bug found, failed to clear views in acquired ViewContainerRef`); - } + private static _assignMessageAndModel( + confirmationComponentRef: ComponentRef, + model: ConfirmationModelImpl, + ): { status: boolean; error?: string } { + let isMessageComponentCreated = false; - this.dynamicComponentsService.destroyUniqueViewContainerRef(this._acquiredViewContainerRef.uuid); + if (CollectionsUtil.isDefined(model.messageComponent)) { + try { + const messageComponentRef = + confirmationComponentRef.instance.viewContainerRef.createComponent( + model.messageComponent, + ); - this._acquiredViewContainerRef = null; - } + isMessageComponentCreated = true; - private static _assignMessageAndModel( - confirmationComponentRef: ComponentRef, - model: ConfirmationModelImpl - ): { status: boolean; error?: string } { - let isMessageComponentCreated = false; - - if (CollectionsUtil.isDefined(model.messageComponent)) { - try { - const messageComponentRef = confirmationComponentRef.instance.viewContainerRef.createComponent(model.messageComponent); - - isMessageComponentCreated = true; - - if (CollectionsUtil.isStringWithContent(model.messageCode)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - messageComponentRef.instance.messageCode = model.messageCode; - } - } catch (e) { - const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create Component instance for Confirmation Message`; - console.error(errorMessage); - - return { - status: false, - error: errorMessage - }; - } + if (CollectionsUtil.isStringWithContent(model.messageCode)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + messageComponentRef.instance.messageCode = model.messageCode; } - - confirmationComponentRef.instance.model = new ConfirmationModelImpl({ - ...model, - messageComponent: undefined, - messageCode: undefined, - message: isMessageComponentCreated ? undefined : model.message - }); + } catch (e) { + const errorMessage = `${ConfirmationService.CLASS_NAME}: Potential bug found, Failed to create Component instance for Confirmation Message`; + console.error(errorMessage); return { - status: true + status: false, + error: errorMessage, }; + } } + + confirmationComponentRef.instance.model = new ConfirmationModelImpl({ + ...model, + messageComponent: undefined, + messageCode: undefined, + message: isMessageComponentCreated ? undefined : model.message, + }); + + return { + status: true, + }; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/index.ts index d02b1771b2..2768970859 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/confirmation/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './confirmation.service'; +export * from "./confirmation.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/directives.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/directives.module.ts index 2e62badd40..1be8ec4837 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/directives.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/directives.module.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule } from "@angular/router"; @NgModule({ - imports: [CommonModule, RouterModule], - declarations: [], - exports: [] + imports: [CommonModule, RouterModule], + declarations: [], + exports: [], }) export class DirectivesModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/public-api.ts index aea8562ca8..ee3bae4410 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/directives/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './directives.module'; +export * from "./directives.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.spec.ts index ac3d41b5d4..a7467a7882 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.spec.ts @@ -3,38 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ViewContainerRef } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ViewContainerRef } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { DynamicContainerComponent } from './dynamic-container.component'; +import { DynamicContainerComponent } from "./dynamic-container.component"; -describe('DynamicContainerComponent', () => { - let component: DynamicContainerComponent; - let fixture: ComponentFixture; +describe("DynamicContainerComponent", () => { + let component: DynamicContainerComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DynamicContainerComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DynamicContainerComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(DynamicContainerComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(DynamicContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should verify component is created', () => { - // Then - expect(component).toBeTruthy(); - }); + it("should verify component is created", () => { + // Then + expect(component).toBeTruthy(); + }); - describe('Properties::', () => { - describe('|viewContainerRef|', () => { - it('should verify value is populate after component is created', () => { - // Then - expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); - }); - }); + describe("Properties::", () => { + describe("|viewContainerRef|", () => { + it("should verify value is populate after component is created", () => { + // Then + expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.ts index a193fceac9..a26b74ed3f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-container/dynamic-container.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; /** * ** Dynamic Container Component is created only once upon {@link DynamicComponentsService} initialization @@ -13,17 +13,20 @@ import { Component, ViewChild, ViewContainerRef } from '@angular/core'; * is stored as ViewContainerRef in {@link DynamicComponentsService} for lifetime of the service as singleton. */ @Component({ - selector: 'shared-dynamic-components-container', - templateUrl: './dynamic-container.component.html', - styleUrls: ['./dynamic-container.component.scss'] + selector: "shared-dynamic-components-container", + templateUrl: "./dynamic-container.component.html", + styleUrls: ["./dynamic-container.component.scss"], }) export class DynamicContainerComponent { - /** - * ** ViewContainerRef reference that is used as point where {@link DynamicComponentsService} insert contextual {@link DynamicContextComponent} - * one for every single UUID when invokers call {@link DynamicComponentsService.getUniqueViewContainerRef}. - * - * - Reference is singleton, and it is retrieved only once upon {@link DynamicComponentsService} initialization. - */ - @ViewChild('dynamicComponentsContainer', { read: ViewContainerRef, static: true }) - public readonly viewContainerRef: ViewContainerRef; + /** + * ** ViewContainerRef reference that is used as point where {@link DynamicComponentsService} insert contextual {@link DynamicContextComponent} + * one for every single UUID when invokers call {@link DynamicComponentsService.getUniqueViewContainerRef}. + * + * - Reference is singleton, and it is retrieved only once upon {@link DynamicComponentsService} initialization. + */ + @ViewChild("dynamicComponentsContainer", { + read: ViewContainerRef, + static: true, + }) + public readonly viewContainerRef: ViewContainerRef; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.spec.ts index c951a1bc00..0c8bbc85f2 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.spec.ts @@ -3,38 +3,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ViewContainerRef } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ViewContainerRef } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { DynamicContextComponent } from './dynamic-context.component'; +import { DynamicContextComponent } from "./dynamic-context.component"; -describe('DynamicContextComponent', () => { - let component: DynamicContextComponent; - let fixture: ComponentFixture; +describe("DynamicContextComponent", () => { + let component: DynamicContextComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DynamicContextComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DynamicContextComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(DynamicContextComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(DynamicContextComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should verify component is created', () => { - // Then - expect(component).toBeTruthy(); - }); + it("should verify component is created", () => { + // Then + expect(component).toBeTruthy(); + }); - describe('Properties::', () => { - describe('|viewContainerRef|', () => { - it('should verify value is populate after component is created', () => { - // Then - expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); - }); - }); + describe("Properties::", () => { + describe("|viewContainerRef|", () => { + it("should verify value is populate after component is created", () => { + // Then + expect(component.viewContainerRef).toBeInstanceOf(ViewContainerRef); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.ts index 044692f695..3c26831829 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/dynamic-context/dynamic-context.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; /** * ** Dynamic Context Component that is created for every UUID in {@link DynamicComponentsService}. @@ -13,17 +13,20 @@ import { Component, ViewChild, ViewContainerRef } from '@angular/core'; * to meet their needs for creating dynamic Components. */ @Component({ - selector: 'shared-dynamic-components-context', - templateUrl: './dynamic-context.component.html', - styleUrls: ['./dynamic-context.component.scss'] + selector: "shared-dynamic-components-context", + templateUrl: "./dynamic-context.component.html", + styleUrls: ["./dynamic-context.component.scss"], }) export class DynamicContextComponent { - /** - * ** ViewContainerRef reference that is used as point where invokers of {@link DynamicComponentsService.getUniqueViewContainerRef} - * retrieve reference of, and could insert their Components. - * - * - Reference is totally contextual and unique for every single UUID and other invokers won't be bothered. - */ - @ViewChild('dynamicComponentsContext', { read: ViewContainerRef, static: true }) - public readonly viewContainerRef: ViewContainerRef; + /** + * ** ViewContainerRef reference that is used as point where invokers of {@link DynamicComponentsService.getUniqueViewContainerRef} + * retrieve reference of, and could insert their Components. + * + * - Reference is totally contextual and unique for every single UUID and other invokers won't be bothered. + */ + @ViewChild("dynamicComponentsContext", { + read: ViewContainerRef, + static: true, + }) + public readonly viewContainerRef: ViewContainerRef; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/index.ts index 6fed11ab34..315c782567 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/components/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './dynamic-container/dynamic-container.component'; -export * from './dynamic-context/dynamic-context.component'; +export * from "./dynamic-container/dynamic-container.component"; +export * from "./dynamic-context/dynamic-context.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/dynamic-components.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/dynamic-components.module.ts index f5fbf6a18e..f27612e7ed 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/dynamic-components.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/dynamic-components.module.ts @@ -3,10 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; -import { DynamicContainerComponent, DynamicContextComponent } from './components'; +import { + DynamicContainerComponent, + DynamicContextComponent, +} from "./components"; /** * ** Dynamic Components module @@ -14,8 +17,8 @@ import { DynamicContainerComponent, DynamicContextComponent } from './components * @author gorankokin */ @NgModule({ - imports: [CommonModule], - declarations: [DynamicContainerComponent, DynamicContextComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + imports: [CommonModule], + declarations: [DynamicContainerComponent, DynamicContextComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class DynamicComponentsModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/index.ts index de62709897..cda649c573 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './services'; +export * from "./services"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/public-api.ts index 75058a042a..1ca9280fa3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/public-api.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { DynamicComponentsService } from './services'; -export { DynamicComponentsModule } from './dynamic-components.module'; +export { DynamicComponentsService } from "./services"; +export { DynamicComponentsModule } from "./dynamic-components.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.spec.ts index edbd11054f..bc5ff6e939 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.spec.ts @@ -5,442 +5,546 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/dot-notation */ -import { ChangeDetectorRef, ComponentRef, ViewContainerRef, ViewRef } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; - -import { CollectionsUtil } from '../../../utils'; +import { + ChangeDetectorRef, + ComponentRef, + ViewContainerRef, + ViewRef, +} from "@angular/core"; +import { TestBed } from "@angular/core/testing"; + +import { CollectionsUtil } from "../../../utils"; + +import { TaurusObject } from "../../../common"; + +import { CallFake } from "../../../unit-testing"; + +import { + DynamicContainerComponent, + DynamicContextComponent, +} from "../components"; + +import { DynamicComponentsService } from "./dynamic-components.service"; + +describe("DynamicComponentsService", () => { + let appRootViewContainerRefStub: jasmine.SpyObj; + + let containerChangeDetectorRefStub: jasmine.SpyObj; + let containerViewContainerRefStub: jasmine.SpyObj; + let containerComponentRefStub: jasmine.SpyObj< + ComponentRef + >; + let containerComponentHostViewStub: jasmine.SpyObj; + + let contextChangeDetectorRefStub: jasmine.SpyObj; + let contextViewContainerRefStub: jasmine.SpyObj; + let contextComponentRefStub: jasmine.SpyObj< + ComponentRef + >; + let contextComponentHostViewStub: jasmine.SpyObj; + + let service: DynamicComponentsService; + + beforeEach(() => { + appRootViewContainerRefStub = jasmine.createSpyObj( + "appRootViewContainerRefStub", + ["createComponent", "clear"], + ); + + containerChangeDetectorRefStub = jasmine.createSpyObj( + "dynamicContainerChangeDetectorRefStub", + ["detectChanges"], + ); + containerViewContainerRefStub = jasmine.createSpyObj( + "dynamicContainerViewContainerRefStub", + ["createComponent", "clear"], + ); + containerComponentHostViewStub = jasmine.createSpyObj( + "containerComponentHostViewStub", + ["destroy"], + { + destroyed: false, + }, + ); + containerComponentRefStub = jasmine.createSpyObj< + ComponentRef + >("dynamicContainerComponentRefStub", ["destroy"], { + changeDetectorRef: containerChangeDetectorRefStub, + instance: { + viewContainerRef: containerViewContainerRefStub, + }, + hostView: containerComponentHostViewStub, + }); -import { TaurusObject } from '../../../common'; + contextChangeDetectorRefStub = jasmine.createSpyObj( + "dynamicContextChangeDetectorRefStub", + ["detectChanges"], + ); + contextViewContainerRefStub = jasmine.createSpyObj( + "dynamicContextViewContainerRefStub", + ["createComponent", "clear"], + ); + contextComponentHostViewStub = jasmine.createSpyObj( + "contextComponentHostViewStub", + ["destroy"], + { + destroyed: false, + }, + ); + contextComponentRefStub = jasmine.createSpyObj< + ComponentRef + >("dynamicContextComponentRefStub", ["destroy"], { + changeDetectorRef: contextChangeDetectorRefStub, + instance: { + viewContainerRef: contextViewContainerRefStub, + }, + hostView: contextComponentHostViewStub, + }); -import { CallFake } from '../../../unit-testing'; + TestBed.configureTestingModule({ + providers: [DynamicComponentsService], + }); -import { DynamicContainerComponent, DynamicContextComponent } from '../components'; + service = TestBed.inject(DynamicComponentsService); + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + expect(service).toBeInstanceOf(DynamicComponentsService); + expect(service).toBeInstanceOf(TaurusObject); + }); + + describe("Statics::", () => { + describe("Properties::", () => { + describe("|CLASS_NAME|", () => { + it("should verify the value", () => { + // Then + expect(DynamicComponentsService.CLASS_NAME).toEqual( + "DynamicComponentsService", + ); + }); + }); + }); + }); -import { DynamicComponentsService } from './dynamic-components.service'; + describe("Properties::", () => { + describe("|objectUUID|", () => { + it("should verify value is DynamicComponentsService", () => { + // Then + expect(/^DynamicComponentsService/.test(service.objectUUID)).toBeTrue(); + }); + }); + }); + + describe("Methods::", () => { + describe("|initialize|", () => { + const parameters: Array<[string, ViewContainerRef]> = [ + ["null", null], + ["undefined", undefined], + [ + "ViewContainerRef", + jasmine.createSpyObj("viewContainerRefStub", [ + "createComponent", + ]), + ], + ]; + + for (const [context, viewContainerRef] of parameters) { + it(`should verify invoking won't throw error when providing ${context}`, () => { + // Given + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + + // When/Then + expect(() => service.initialize(viewContainerRef)).not.toThrowError(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + } + }); -describe('DynamicComponentsService', () => { - let appRootViewContainerRefStub: jasmine.SpyObj; + describe("|getUniqueViewContainerRef|", () => { + it("should verify will return null, when there is no app root ViewContainerRef", () => { + // When + const viewContainerRef1 = service.getUniqueViewContainerRef(); + const viewContainerRef2 = service.getUniqueViewContainerRef( + CollectionsUtil.generateUUID(), + ); - let containerChangeDetectorRefStub: jasmine.SpyObj; - let containerViewContainerRefStub: jasmine.SpyObj; - let containerComponentRefStub: jasmine.SpyObj>; - let containerComponentHostViewStub: jasmine.SpyObj; + // Then + expect(viewContainerRef1).toBeNull(); + expect(viewContainerRef2).toBeNull(); + }); + + it("should verify will return null, when trying to create DynamicContainerComponent, but factory throws error", () => { + // Given + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + appRootViewContainerRefStub.createComponent.and.throwError( + new Error("error"), + ); + service.initialize(appRootViewContainerRefStub); - let contextChangeDetectorRefStub: jasmine.SpyObj; - let contextViewContainerRefStub: jasmine.SpyObj; - let contextComponentRefStub: jasmine.SpyObj>; - let contextComponentHostViewStub: jasmine.SpyObj; + // When + const acquiredReference = service.getUniqueViewContainerRef(); - let service: DynamicComponentsService; + // Then + expect(acquiredReference).toBeNull(); + expect( + appRootViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContainerComponent as any); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContainerComponent`, + ); + }); + + it("should verify will return null, when DynamicContainerComponent is created but validation fail, ViewContainerRef is missing", () => { + // Given + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + // clear ViewContainerRef from stub + // @ts-ignore + containerComponentRefStub.instance["viewContainerRef"] = null; + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); - beforeEach(() => { - appRootViewContainerRefStub = jasmine.createSpyObj('appRootViewContainerRefStub', ['createComponent', 'clear']); + // When + const acquiredReference = service.getUniqueViewContainerRef(); - containerChangeDetectorRefStub = jasmine.createSpyObj('dynamicContainerChangeDetectorRefStub', [ - 'detectChanges' - ]); - containerViewContainerRefStub = jasmine.createSpyObj('dynamicContainerViewContainerRefStub', [ - 'createComponent', - 'clear' - ]); - containerComponentHostViewStub = jasmine.createSpyObj('containerComponentHostViewStub', ['destroy'], { - destroyed: false - }); - containerComponentRefStub = jasmine.createSpyObj>( - 'dynamicContainerComponentRefStub', - ['destroy'], - { - changeDetectorRef: containerChangeDetectorRefStub, - instance: { - viewContainerRef: containerViewContainerRefStub - }, - hostView: containerComponentHostViewStub - } + // Then + expect(acquiredReference).toBeNull(); + expect( + appRootViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContainerComponent as any); + expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Service is not initialized correctly ` + + `or during initialization failed to create instance of DynamicContainerComponent`, ); + }); - contextChangeDetectorRefStub = jasmine.createSpyObj('dynamicContextChangeDetectorRefStub', ['detectChanges']); - contextViewContainerRefStub = jasmine.createSpyObj('dynamicContextViewContainerRefStub', [ - 'createComponent', - 'clear' - ]); - contextComponentHostViewStub = jasmine.createSpyObj('contextComponentHostViewStub', ['destroy'], { - destroyed: false - }); - contextComponentRefStub = jasmine.createSpyObj>( - 'dynamicContextComponentRefStub', - ['destroy'], - { - changeDetectorRef: contextChangeDetectorRefStub, - instance: { - viewContainerRef: contextViewContainerRefStub - }, - hostView: contextComponentHostViewStub - } + it("should verify will return null, when trying to create DynamicContextComponent, but factory throws error", () => { + // Given + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.throwError( + new Error("Error"), ); + service.initialize(appRootViewContainerRefStub); - TestBed.configureTestingModule({ - providers: [DynamicComponentsService] - }); + // When + const acquiredReference = service.getUniqueViewContainerRef(); - service = TestBed.inject(DynamicComponentsService); - }); + // Then + expect(acquiredReference).toBeNull(); + expect( + appRootViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContainerComponent as any); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContextComponent as any); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContextComponent`, + ); + }); + + it("should verify will return null, when DynamicContextComponent is created but validation fail, ViewContainerRef is missing", () => { + // Given + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + // clear ViewContainerRef from stub + // @ts-ignore + contextComponentRefStub.instance["viewContainerRef"] = null; + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); + + // When + const acquiredReference = service.getUniqueViewContainerRef(); - it('should verify instance is created', () => { // Then - expect(service).toBeDefined(); - expect(service).toBeInstanceOf(DynamicComponentsService); - expect(service).toBeInstanceOf(TaurusObject); - }); + expect(acquiredReference).toBeNull(); + expect( + appRootViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContainerComponent as any); + expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContextComponent as any); + expect(contextChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to retrieve context instance of DynamicContextComponent`, + ); + }); + + it("should verify will return expected value uuid, ViewContainerRef and ViewRef", () => { + // Given + const uuid = CollectionsUtil.generateUUID(); + spyOn(CollectionsUtil, "generateUUID").and.returnValue(uuid); + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); + + // When + const acquiredReference = service.getUniqueViewContainerRef(); - describe('Statics::', () => { - describe('Properties::', () => { - describe('|CLASS_NAME|', () => { - it('should verify the value', () => { - // Then - expect(DynamicComponentsService.CLASS_NAME).toEqual('DynamicComponentsService'); - }); - }); + // Then + expect(acquiredReference).toEqual({ + uuid, + viewContainerRef: contextViewContainerRefStub, + hostView: contextComponentHostViewStub, }); - }); + expect( + appRootViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContainerComponent as any); + expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledWith(DynamicContextComponent as any); + expect(contextChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); + }); + + it("should verify will retrieve existing uuid, ViewContainerRef and ViewRef", () => { + // Given + const uuid1 = CollectionsUtil.generateUUID(); + const uuid2 = CollectionsUtil.generateUUID(); + spyOn(CollectionsUtil, "generateUUID").and.returnValues(uuid1, uuid2); + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); - describe('Properties::', () => { - describe('|objectUUID|', () => { - it('should verify value is DynamicComponentsService', () => { - // Then - expect(/^DynamicComponentsService/.test(service.objectUUID)).toBeTrue(); - }); + // When + const acquiredReference1 = service.getUniqueViewContainerRef(); + const acquiredReference2 = service.getUniqueViewContainerRef(uuid1); + const acquiredReference3 = service.getUniqueViewContainerRef(); + const acquiredReference4 = service.getUniqueViewContainerRef(uuid2); + + // Then + expect(acquiredReference1).toEqual({ + uuid: uuid1, + viewContainerRef: contextViewContainerRefStub, + hostView: contextComponentHostViewStub, + }); + expect(acquiredReference2).toEqual({ + uuid: uuid1, + viewContainerRef: contextViewContainerRefStub, + hostView: contextComponentHostViewStub, + }); + expect(acquiredReference3).toEqual({ + uuid: uuid2, + viewContainerRef: contextViewContainerRefStub, + hostView: contextComponentHostViewStub, + }); + expect(acquiredReference4).toEqual({ + uuid: uuid2, + viewContainerRef: contextViewContainerRefStub, + hostView: contextComponentHostViewStub, }); + expect( + appRootViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(1); + expect( + containerChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(1); + expect( + containerViewContainerRefStub.createComponent, + ).toHaveBeenCalledTimes(2); + expect( + contextChangeDetectorRefStub.detectChanges, + ).toHaveBeenCalledTimes(2); + }); }); - describe('Methods::', () => { - describe('|initialize|', () => { - const parameters: Array<[string, ViewContainerRef]> = [ - ['null', null], - ['undefined', undefined], - ['ViewContainerRef', jasmine.createSpyObj('viewContainerRefStub', ['createComponent'])] - ]; - - for (const [context, viewContainerRef] of parameters) { - it(`should verify invoking won't throw error when providing ${context}`, () => { - // Given - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - - // When/Then - expect(() => service.initialize(viewContainerRef)).not.toThrowError(); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); - } - }); + describe("|destroyUniqueViewContainerRef|", () => { + it("should verify will return null when ViewContainerRef cannot be found for provided UUID", () => { + // Given + const uuid = CollectionsUtil.generateUUID(); - describe('|getUniqueViewContainerRef|', () => { - it('should verify will return null, when there is no app root ViewContainerRef', () => { - // When - const viewContainerRef1 = service.getUniqueViewContainerRef(); - const viewContainerRef2 = service.getUniqueViewContainerRef(CollectionsUtil.generateUUID()); - - // Then - expect(viewContainerRef1).toBeNull(); - expect(viewContainerRef2).toBeNull(); - }); - - it('should verify will return null, when trying to create DynamicContainerComponent, but factory throws error', () => { - // Given - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - appRootViewContainerRefStub.createComponent.and.throwError(new Error('error')); - service.initialize(appRootViewContainerRefStub); - - // When - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then - expect(acquiredReference).toBeNull(); - expect(appRootViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContainerComponent as any); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContainerComponent` - ); - }); - - it('should verify will return null, when DynamicContainerComponent is created but validation fail, ViewContainerRef is missing', () => { - // Given - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - // clear ViewContainerRef from stub - // @ts-ignore - containerComponentRefStub.instance['viewContainerRef'] = null; - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then - expect(acquiredReference).toBeNull(); - expect(appRootViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContainerComponent as any); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Service is not initialized correctly ` + - `or during initialization failed to create instance of DynamicContainerComponent` - ); - }); - - it('should verify will return null, when trying to create DynamicContextComponent, but factory throws error', () => { - // Given - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.throwError(new Error('Error')); - service.initialize(appRootViewContainerRefStub); - - // When - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then - expect(acquiredReference).toBeNull(); - expect(appRootViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContainerComponent as any); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContextComponent as any); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContextComponent` - ); - }); - - it('should verify will return null, when DynamicContextComponent is created but validation fail, ViewContainerRef is missing', () => { - // Given - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - // clear ViewContainerRef from stub - // @ts-ignore - contextComponentRefStub.instance['viewContainerRef'] = null; - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then - expect(acquiredReference).toBeNull(); - expect(appRootViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContainerComponent as any); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContextComponent as any); - expect(contextChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to retrieve context instance of DynamicContextComponent` - ); - }); - - it('should verify will return expected value uuid, ViewContainerRef and ViewRef', () => { - // Given - const uuid = CollectionsUtil.generateUUID(); - spyOn(CollectionsUtil, 'generateUUID').and.returnValue(uuid); - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then - expect(acquiredReference).toEqual({ - uuid, - viewContainerRef: contextViewContainerRefStub, - hostView: contextComponentHostViewStub - }); - expect(appRootViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContainerComponent as any); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledWith(DynamicContextComponent as any); - expect(contextChangeDetectorRefStub.detectChanges).toHaveBeenCalled(); - }); - - it('should verify will retrieve existing uuid, ViewContainerRef and ViewRef', () => { - // Given - const uuid1 = CollectionsUtil.generateUUID(); - const uuid2 = CollectionsUtil.generateUUID(); - spyOn(CollectionsUtil, 'generateUUID').and.returnValues(uuid1, uuid2); - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When - const acquiredReference1 = service.getUniqueViewContainerRef(); - const acquiredReference2 = service.getUniqueViewContainerRef(uuid1); - const acquiredReference3 = service.getUniqueViewContainerRef(); - const acquiredReference4 = service.getUniqueViewContainerRef(uuid2); - - // Then - expect(acquiredReference1).toEqual({ - uuid: uuid1, - viewContainerRef: contextViewContainerRefStub, - hostView: contextComponentHostViewStub - }); - expect(acquiredReference2).toEqual({ - uuid: uuid1, - viewContainerRef: contextViewContainerRefStub, - hostView: contextComponentHostViewStub - }); - expect(acquiredReference3).toEqual({ - uuid: uuid2, - viewContainerRef: contextViewContainerRefStub, - hostView: contextComponentHostViewStub - }); - expect(acquiredReference4).toEqual({ - uuid: uuid2, - viewContainerRef: contextViewContainerRefStub, - hostView: contextComponentHostViewStub - }); - expect(appRootViewContainerRefStub.createComponent).toHaveBeenCalledTimes(1); - expect(containerChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(1); - expect(containerViewContainerRefStub.createComponent).toHaveBeenCalledTimes(2); - expect(contextChangeDetectorRefStub.detectChanges).toHaveBeenCalledTimes(2); - }); - }); + // When + const isDestroyed = service.destroyUniqueViewContainerRef(uuid); - describe('|destroyUniqueViewContainerRef|', () => { - it('should verify will return null when ViewContainerRef cannot be found for provided UUID', () => { - // Given - const uuid = CollectionsUtil.generateUUID(); - - // When - const isDestroyed = service.destroyUniqueViewContainerRef(uuid); - - // Then - expect(isDestroyed).toBeNull(); - }); - - it('should verify will return false if destroying ViewContainerRef throws error', () => { - // Given - const uuid = CollectionsUtil.generateUUID(); - spyOn(CollectionsUtil, 'generateUUID').and.returnValue(uuid); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - contextComponentRefStub.destroy.and.throwError(new Error('Error')); - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When 1 - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then 1 - expect(acquiredReference).toBeDefined(); - expect(service['_uniqueComponentRefsStore'].size).toEqual(1); - expect(service['_uniqueComponentRefsStore'].get(uuid)).toEqual(contextComponentRefStub); - - // When - const isDestroyed = service.destroyUniqueViewContainerRef(uuid); - - // Then - expect(isDestroyed).toBeFalse(); - expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(service['_uniqueComponentRefsStore'].size).toEqual(1); - expect(service['_uniqueComponentRefsStore'].get(uuid)).toEqual(contextComponentRefStub); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to destroy unique ViewContainerRef instance in DynamicContextComponent` - ); - }); - - it('should verify will return true if destroying ViewContainerRef is successful', () => { - // Given - const uuid = CollectionsUtil.generateUUID(); - spyOn(CollectionsUtil, 'generateUUID').and.returnValue(uuid); - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When 1 - const acquiredReference = service.getUniqueViewContainerRef(); - - // Then 1 - expect(acquiredReference).toBeDefined(); - expect(service['_uniqueComponentRefsStore'].size).toEqual(1); - expect(service['_uniqueComponentRefsStore'].get(uuid)).toEqual(contextComponentRefStub); - - // When - const isDestroyed = service.destroyUniqueViewContainerRef(uuid); - - // Then - expect(isDestroyed).toBeTrue(); - expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(service['_uniqueComponentRefsStore'].size).toEqual(0); - }); - }); + // Then + expect(isDestroyed).toBeNull(); + }); + + it("should verify will return false if destroying ViewContainerRef throws error", () => { + // Given + const uuid = CollectionsUtil.generateUUID(); + spyOn(CollectionsUtil, "generateUUID").and.returnValue(uuid); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + contextComponentRefStub.destroy.and.throwError(new Error("Error")); + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); - describe('|ngOnDestroy|', () => { - it('should verify will clear all resources when invoked', () => { - // Given - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - service.initialize(appRootViewContainerRefStub); - - // When 1 - const acquiredReference1 = service.getUniqueViewContainerRef(); - const acquiredReference2 = service.getUniqueViewContainerRef(); - const acquiredReference3 = service.getUniqueViewContainerRef(); - - // Then 1 - expect(contextComponentHostViewStub.destroy).not.toHaveBeenCalled(); - expect(acquiredReference1).toBeDefined(); - expect(acquiredReference2).toBeDefined(); - expect(acquiredReference3).toBeDefined(); - expect(service['_uniqueComponentRefsStore'].size).toEqual(3); - - // When 2 - service.ngOnDestroy(); - - // Then 2 - expect(service['_uniqueComponentRefsStore'].size).toEqual(0); - expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(3); - expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(appRootViewContainerRefStub.clear).toHaveBeenCalledTimes(1); - }); - - it(`should verify won't throw error when invoked and there is error thrown inside`, () => { - // Given - const uuid1 = CollectionsUtil.generateUUID(); - const uuid2 = CollectionsUtil.generateUUID(); - const uuid3 = CollectionsUtil.generateUUID(); - spyOn(CollectionsUtil, 'generateUUID').and.returnValues(uuid1, uuid2, uuid3); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - containerComponentRefStub.destroy.and.throwError(new Error('Error 1')); - appRootViewContainerRefStub.createComponent.and.returnValue(containerComponentRefStub); - contextComponentRefStub.destroy.and.throwError(new Error('Error 2')); - containerViewContainerRefStub.createComponent.and.returnValue(contextComponentRefStub); - appRootViewContainerRefStub.clear.and.throwError(new Error('Error 3')); - service.initialize(appRootViewContainerRefStub); - - // When 1 - const acquiredReference1 = service.getUniqueViewContainerRef(); - const acquiredReference2 = service.getUniqueViewContainerRef(); - const acquiredReference3 = service.getUniqueViewContainerRef(); - - // Then 1 - expect(contextComponentHostViewStub.destroy).not.toHaveBeenCalled(); - expect(acquiredReference1).toBeDefined(); - expect(acquiredReference2).toBeDefined(); - expect(acquiredReference3).toBeDefined(); - expect(service['_uniqueComponentRefsStore'].size).toEqual(3); - - // When 2 - service.ngOnDestroy(); - - // Then 2 - expect(service['_uniqueComponentRefsStore'].size).toEqual(0); - expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(3); - expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); - expect(appRootViewContainerRefStub.clear).toHaveBeenCalledTimes(1); - - expect(consoleErrorSpy).toHaveBeenCalledTimes(5); - expect(consoleErrorSpy.calls.argsFor(0)).toEqual([ - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid1}` - ]); - expect(consoleErrorSpy.calls.argsFor(1)).toEqual([ - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid2}` - ]); - expect(consoleErrorSpy.calls.argsFor(2)).toEqual([ - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid3}` - ]); - expect(consoleErrorSpy.calls.argsFor(3)).toEqual([ - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy DynamicContextContainer ref` - ]); - expect(consoleErrorSpy.calls.argsFor(4)).toEqual([ - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy root ViewContainerRef` - ]); - }); - }); + // When 1 + const acquiredReference = service.getUniqueViewContainerRef(); + + // Then 1 + expect(acquiredReference).toBeDefined(); + expect(service["_uniqueComponentRefsStore"].size).toEqual(1); + expect(service["_uniqueComponentRefsStore"].get(uuid)).toEqual( + contextComponentRefStub, + ); + + // When + const isDestroyed = service.destroyUniqueViewContainerRef(uuid); + + // Then + expect(isDestroyed).toBeFalse(); + expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(service["_uniqueComponentRefsStore"].size).toEqual(1); + expect(service["_uniqueComponentRefsStore"].get(uuid)).toEqual( + contextComponentRefStub, + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to destroy unique ViewContainerRef instance in DynamicContextComponent`, + ); + }); + + it("should verify will return true if destroying ViewContainerRef is successful", () => { + // Given + const uuid = CollectionsUtil.generateUUID(); + spyOn(CollectionsUtil, "generateUUID").and.returnValue(uuid); + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); + + // When 1 + const acquiredReference = service.getUniqueViewContainerRef(); + + // Then 1 + expect(acquiredReference).toBeDefined(); + expect(service["_uniqueComponentRefsStore"].size).toEqual(1); + expect(service["_uniqueComponentRefsStore"].get(uuid)).toEqual( + contextComponentRefStub, + ); + + // When + const isDestroyed = service.destroyUniqueViewContainerRef(uuid); + + // Then + expect(isDestroyed).toBeTrue(); + expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(service["_uniqueComponentRefsStore"].size).toEqual(0); + }); + }); + + describe("|ngOnDestroy|", () => { + it("should verify will clear all resources when invoked", () => { + // Given + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + service.initialize(appRootViewContainerRefStub); + + // When 1 + const acquiredReference1 = service.getUniqueViewContainerRef(); + const acquiredReference2 = service.getUniqueViewContainerRef(); + const acquiredReference3 = service.getUniqueViewContainerRef(); + + // Then 1 + expect(contextComponentHostViewStub.destroy).not.toHaveBeenCalled(); + expect(acquiredReference1).toBeDefined(); + expect(acquiredReference2).toBeDefined(); + expect(acquiredReference3).toBeDefined(); + expect(service["_uniqueComponentRefsStore"].size).toEqual(3); + + // When 2 + service.ngOnDestroy(); + + // Then 2 + expect(service["_uniqueComponentRefsStore"].size).toEqual(0); + expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(3); + expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(appRootViewContainerRefStub.clear).toHaveBeenCalledTimes(1); + }); + + it(`should verify won't throw error when invoked and there is error thrown inside`, () => { + // Given + const uuid1 = CollectionsUtil.generateUUID(); + const uuid2 = CollectionsUtil.generateUUID(); + const uuid3 = CollectionsUtil.generateUUID(); + spyOn(CollectionsUtil, "generateUUID").and.returnValues( + uuid1, + uuid2, + uuid3, + ); + const consoleErrorSpy = spyOn(console, "error").and.callFake(CallFake); + containerComponentRefStub.destroy.and.throwError(new Error("Error 1")); + appRootViewContainerRefStub.createComponent.and.returnValue( + containerComponentRefStub, + ); + contextComponentRefStub.destroy.and.throwError(new Error("Error 2")); + containerViewContainerRefStub.createComponent.and.returnValue( + contextComponentRefStub, + ); + appRootViewContainerRefStub.clear.and.throwError(new Error("Error 3")); + service.initialize(appRootViewContainerRefStub); + + // When 1 + const acquiredReference1 = service.getUniqueViewContainerRef(); + const acquiredReference2 = service.getUniqueViewContainerRef(); + const acquiredReference3 = service.getUniqueViewContainerRef(); + + // Then 1 + expect(contextComponentHostViewStub.destroy).not.toHaveBeenCalled(); + expect(acquiredReference1).toBeDefined(); + expect(acquiredReference2).toBeDefined(); + expect(acquiredReference3).toBeDefined(); + expect(service["_uniqueComponentRefsStore"].size).toEqual(3); + + // When 2 + service.ngOnDestroy(); + + // Then 2 + expect(service["_uniqueComponentRefsStore"].size).toEqual(0); + expect(contextComponentRefStub.destroy).toHaveBeenCalledTimes(3); + expect(containerComponentRefStub.destroy).toHaveBeenCalledTimes(1); + expect(appRootViewContainerRefStub.clear).toHaveBeenCalledTimes(1); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(5); + expect(consoleErrorSpy.calls.argsFor(0)).toEqual([ + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid1}`, + ]); + expect(consoleErrorSpy.calls.argsFor(1)).toEqual([ + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid2}`, + ]); + expect(consoleErrorSpy.calls.argsFor(2)).toEqual([ + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid3}`, + ]); + expect(consoleErrorSpy.calls.argsFor(3)).toEqual([ + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy DynamicContextContainer ref`, + ]); + expect(consoleErrorSpy.calls.argsFor(4)).toEqual([ + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy root ViewContainerRef`, + ]); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.ts index aceec5610b..0971198cd6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/dynamic-components.service.ts @@ -3,274 +3,324 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentRef, Injectable, OnDestroy, ViewContainerRef, ViewRef } from '@angular/core'; +import { + ComponentRef, + Injectable, + OnDestroy, + ViewContainerRef, + ViewRef, +} from "@angular/core"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { DynamicContainerComponent, DynamicContextComponent } from '../components'; +import { + DynamicContainerComponent, + DynamicContextComponent, +} from "../components"; /** * ** Internal service model. */ interface AcquireViewContainerRefModel { - /** - * ** ViewContainerRef uuid. - * - * - ViewContainerRefs could be reused by providing issued UUID. - * - References are stored in the service Map where for every issued UUID key there is ViewContainerRef behind as value. - */ - uuid: string; - - /** - * ** Unique ViewContainerRef created on behalf of the invoker. - */ - viewContainerRef: ViewContainerRef; - - /** - * ** ViewRef to the unique ViewContainerRef created on behalf of the invoker. - */ - hostView: ViewRef; + /** + * ** ViewContainerRef uuid. + * + * - ViewContainerRefs could be reused by providing issued UUID. + * - References are stored in the service Map where for every issued UUID key there is ViewContainerRef behind as value. + */ + uuid: string; + + /** + * ** Unique ViewContainerRef created on behalf of the invoker. + */ + viewContainerRef: ViewContainerRef; + + /** + * ** ViewRef to the unique ViewContainerRef created on behalf of the invoker. + */ + hostView: ViewRef; } /** * ** Dynamic Components Service that generates ViewContainerRefs in context that could be used once or reused multiple times. */ @Injectable() -export class DynamicComponentsService extends TaurusObject implements OnDestroy { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'DynamicComponentsService'; - - /** - * ** Acquired ViewContainerRef with dependency injection during service initialization {@link DynamicComponentsService.initialize}, - * where Dynamic Container {@link DynamicContainerComponent} will be inserted. - * @private - */ - private _appRootViewContainerRef: ViewContainerRef; - - /** - * ** Dynamic Container reference {@link DynamicContainerComponent}, - * where all contextual Dynamic components {@link DynamicContextComponent} will be inserted. - * @private - */ - private _dynamicContainerRef: ComponentRef; - - /** - * ** Local store where all created Dynamic Context Components are stored under their corresponding issued UUID. - * @private - */ - private readonly _uniqueComponentRefsStore: Map>; - - /** - * ** Constructor. - */ - constructor() { - super(DynamicComponentsService.CLASS_NAME); - - this._uniqueComponentRefsStore = new Map>(); +export class DynamicComponentsService + extends TaurusObject + implements OnDestroy +{ + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "DynamicComponentsService"; + + /** + * ** Acquired ViewContainerRef with dependency injection during service initialization {@link DynamicComponentsService.initialize}, + * where Dynamic Container {@link DynamicContainerComponent} will be inserted. + * @private + */ + private _appRootViewContainerRef: ViewContainerRef; + + /** + * ** Dynamic Container reference {@link DynamicContainerComponent}, + * where all contextual Dynamic components {@link DynamicContextComponent} will be inserted. + * @private + */ + private _dynamicContainerRef: ComponentRef; + + /** + * ** Local store where all created Dynamic Context Components are stored under their corresponding issued UUID. + * @private + */ + private readonly _uniqueComponentRefsStore: Map< + string, + ComponentRef + >; + + /** + * ** Constructor. + */ + constructor() { + super(DynamicComponentsService.CLASS_NAME); + + this._uniqueComponentRefsStore = new Map< + string, + ComponentRef + >(); + } + + /** + * ** Create or retrieve unique ViewContainerRef together with ViewRef and bound UUID. + * + * - if UUID is provided it will try to retrieve such reference if exists, + * otherwise will create new ViewContainerRef which will be stored under provided UUID key. + * - if no UUID provided will proceed to issue new UUID, + * then will create new ViewContainerRef which will be stored under the issued UUID, + * and both together with ViewRef will be returned to the invoker according provided return interface. + * - if some error is thrown in process of ViewContainerRef acquisition, + * service will return null instead of reference and that should be handled on invoker side. + * - Currently there is no automatic garbage collection, but only manual destroy utilizing {@link this.destroyUniqueViewContainerRef}, + * so be careful not to acquire too many unique ViewContainerRef references, + * because they could downgrade Application performance (they are created as Component instances in root of the application). + * - Automatic GC is not currently developed because there is possibility to retrieve existing + * contextual ViewContainerRef instances with issued UUIDs for re-usage, + * or created instances refs could be kept into the invoker scope (context), + * or instances refs could be destroyed using the provided method with issued UUIDs {@link this.destroyUniqueViewContainerRef} + */ + getUniqueViewContainerRef( + requestedUUID?: string, + ): AcquireViewContainerRefModel { + const isContainerComponentSuccessfullyCreated = + this._createDynamicContainerComponent(); + if (!isContainerComponentSuccessfullyCreated) { + return null; } - /** - * ** Create or retrieve unique ViewContainerRef together with ViewRef and bound UUID. - * - * - if UUID is provided it will try to retrieve such reference if exists, - * otherwise will create new ViewContainerRef which will be stored under provided UUID key. - * - if no UUID provided will proceed to issue new UUID, - * then will create new ViewContainerRef which will be stored under the issued UUID, - * and both together with ViewRef will be returned to the invoker according provided return interface. - * - if some error is thrown in process of ViewContainerRef acquisition, - * service will return null instead of reference and that should be handled on invoker side. - * - Currently there is no automatic garbage collection, but only manual destroy utilizing {@link this.destroyUniqueViewContainerRef}, - * so be careful not to acquire too many unique ViewContainerRef references, - * because they could downgrade Application performance (they are created as Component instances in root of the application). - * - Automatic GC is not currently developed because there is possibility to retrieve existing - * contextual ViewContainerRef instances with issued UUIDs for re-usage, - * or created instances refs could be kept into the invoker scope (context), - * or instances refs could be destroyed using the provided method with issued UUIDs {@link this.destroyUniqueViewContainerRef} - */ - getUniqueViewContainerRef(requestedUUID?: string): AcquireViewContainerRefModel { - const isContainerComponentSuccessfullyCreated = this._createDynamicContainerComponent(); - if (!isContainerComponentSuccessfullyCreated) { - return null; - } - - const isContainerComponentHealthy = this._validateDynamicContainerComponent(); - if (!isContainerComponentHealthy) { - return null; - } - - const uuid = DynamicComponentsService._getOrGenerateUUID(requestedUUID); - - if (!this._uniqueComponentRefsStore.has(uuid)) { - const isContextComponentSuccessfullyCreated = this._createDynamicContextComponent(uuid); - if (!isContextComponentSuccessfullyCreated) { - return null; - } - } - - const isContextComponentHealthy = this._validateDynamicContextComponent(uuid); - if (!isContextComponentHealthy) { - return null; - } - - return { - uuid: uuid, - viewContainerRef: this._uniqueComponentRefsStore.get(uuid).instance.viewContainerRef, - hostView: this._uniqueComponentRefsStore.get(uuid).hostView - }; + const isContainerComponentHealthy = + this._validateDynamicContainerComponent(); + if (!isContainerComponentHealthy) { + return null; } - /** - * ** Destroy unique ViewContainerRef for provided UUID. - * - * - If reference is found for provided UUID and is successfully destroyed will return true otherwise false. - * - If reference for provided UUID is not found will return null. - */ - destroyUniqueViewContainerRef(uuid: string): boolean { - if (!this._uniqueComponentRefsStore.has(uuid)) { - return null; - } - - try { - this._uniqueComponentRefsStore.get(uuid).destroy(); - this._uniqueComponentRefsStore.delete(uuid); + const uuid = DynamicComponentsService._getOrGenerateUUID(requestedUUID); - return true; - } catch (e) { - console.error( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to destroy unique ViewContainerRef instance in DynamicContextComponent` - ); - - return false; - } + if (!this._uniqueComponentRefsStore.has(uuid)) { + const isContextComponentSuccessfullyCreated = + this._createDynamicContextComponent(uuid); + if (!isContextComponentSuccessfullyCreated) { + return null; + } } - /** - * ** Initialize service. - * - * - Should be invoked only once. - * - Ideal place for invoking is AppComponent.ngOnInit(). - */ - initialize(viewContainerRef: ViewContainerRef): void { - this._appRootViewContainerRef = viewContainerRef; + const isContextComponentHealthy = + this._validateDynamicContextComponent(uuid); + if (!isContextComponentHealthy) { + return null; } - /** - * @inheritDoc - */ - override ngOnDestroy(): void { - this._clearUniqueComponentsRef(); - this._clearContextContainerRef(); - this._clearAppRootViewContainerRef(); - - super.ngOnDestroy(); + return { + uuid: uuid, + viewContainerRef: + this._uniqueComponentRefsStore.get(uuid).instance.viewContainerRef, + hostView: this._uniqueComponentRefsStore.get(uuid).hostView, + }; + } + + /** + * ** Destroy unique ViewContainerRef for provided UUID. + * + * - If reference is found for provided UUID and is successfully destroyed will return true otherwise false. + * - If reference for provided UUID is not found will return null. + */ + destroyUniqueViewContainerRef(uuid: string): boolean { + if (!this._uniqueComponentRefsStore.has(uuid)) { + return null; } - private _createDynamicContainerComponent(): boolean { - if (!this._appRootViewContainerRef) { - return false; - } - - if (this._dynamicContainerRef && this._dynamicContainerRef.hostView && !this._dynamicContainerRef.hostView.destroyed) { - return true; - } - - try { - this._dynamicContainerRef = this._appRootViewContainerRef.createComponent(DynamicContainerComponent); - this._dynamicContainerRef.changeDetectorRef.detectChanges(); + try { + this._uniqueComponentRefsStore.get(uuid).destroy(); + this._uniqueComponentRefsStore.delete(uuid); - return true; - } catch (e) { - console.error( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContainerComponent` - ); + return true; + } catch (e) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to destroy unique ViewContainerRef instance in DynamicContextComponent`, + ); - return false; - } + return false; } - - private _validateDynamicContainerComponent(): boolean { - if (!(this._dynamicContainerRef && this._dynamicContainerRef.instance && this._dynamicContainerRef.instance.viewContainerRef)) { - console.error( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Service is not initialized correctly ` + - `or during initialization failed to create instance of DynamicContainerComponent` - ); - - return false; - } - - return true; + } + + /** + * ** Initialize service. + * + * - Should be invoked only once. + * - Ideal place for invoking is AppComponent.ngOnInit(). + */ + initialize(viewContainerRef: ViewContainerRef): void { + this._appRootViewContainerRef = viewContainerRef; + } + + /** + * @inheritDoc + */ + override ngOnDestroy(): void { + this._clearUniqueComponentsRef(); + this._clearContextContainerRef(); + this._clearAppRootViewContainerRef(); + + super.ngOnDestroy(); + } + + private _createDynamicContainerComponent(): boolean { + if (!this._appRootViewContainerRef) { + return false; } - private _createDynamicContextComponent(uuid: string): boolean { - try { - const uniqueDynamicComponentRef = this._dynamicContainerRef.instance.viewContainerRef.createComponent(DynamicContextComponent); - uniqueDynamicComponentRef.changeDetectorRef.detectChanges(); - this._uniqueComponentRefsStore.set(uuid, uniqueDynamicComponentRef); - - return true; - } catch (e) { - console.error( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContextComponent` - ); - - return false; - } + if ( + this._dynamicContainerRef && + this._dynamicContainerRef.hostView && + !this._dynamicContainerRef.hostView.destroyed + ) { + return true; } - private _validateDynamicContextComponent(uuid: string): boolean { - const retrievedComponentRef = this._uniqueComponentRefsStore.get(uuid); - if (!(retrievedComponentRef && retrievedComponentRef.instance && retrievedComponentRef.instance.viewContainerRef)) { - console.error( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to retrieve context instance of DynamicContextComponent` - ); + try { + this._dynamicContainerRef = this._appRootViewContainerRef.createComponent( + DynamicContainerComponent, + ); + this._dynamicContainerRef.changeDetectorRef.detectChanges(); - return false; - } + return true; + } catch (e) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContainerComponent`, + ); - return true; + return false; + } + } + + private _validateDynamicContainerComponent(): boolean { + if ( + !( + this._dynamicContainerRef && + this._dynamicContainerRef.instance && + this._dynamicContainerRef.instance.viewContainerRef + ) + ) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Service is not initialized correctly ` + + `or during initialization failed to create instance of DynamicContainerComponent`, + ); + + return false; } - private _clearUniqueComponentsRef(): void { - this._uniqueComponentRefsStore.forEach((componentRef, uuid) => { - try { - if (!componentRef.hostView.destroyed) { - componentRef.destroy(); - } - } catch (e) { - console.error( - `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid}` - ); - } - }); - - this._uniqueComponentRefsStore.clear(); + return true; + } + + private _createDynamicContextComponent(uuid: string): boolean { + try { + const uniqueDynamicComponentRef = + this._dynamicContainerRef.instance.viewContainerRef.createComponent( + DynamicContextComponent, + ); + uniqueDynamicComponentRef.changeDetectorRef.detectChanges(); + this._uniqueComponentRefsStore.set(uuid, uniqueDynamicComponentRef); + + return true; + } catch (e) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to create instance of DynamicContextComponent`, + ); + + return false; + } + } + + private _validateDynamicContextComponent(uuid: string): boolean { + const retrievedComponentRef = this._uniqueComponentRefsStore.get(uuid); + if ( + !( + retrievedComponentRef && + retrievedComponentRef.instance && + retrievedComponentRef.instance.viewContainerRef + ) + ) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, Failed to retrieve context instance of DynamicContextComponent`, + ); + + return false; } - private _clearContextContainerRef(): void { - try { - this._dynamicContainerRef.destroy(); - } catch (e) { - console.error(`${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy DynamicContextContainer ref`); - } + return true; + } - this._dynamicContainerRef = null; + private _clearUniqueComponentsRef(): void { + this._uniqueComponentRefsStore.forEach((componentRef, uuid) => { + try { + if (!componentRef.hostView.destroyed) { + componentRef.destroy(); + } + } catch (e) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy unique component ref ${uuid}`, + ); + } + }); + + this._uniqueComponentRefsStore.clear(); + } + + private _clearContextContainerRef(): void { + try { + this._dynamicContainerRef.destroy(); + } catch (e) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy DynamicContextContainer ref`, + ); } - private _clearAppRootViewContainerRef(): void { - try { - this._appRootViewContainerRef.clear(); - } catch (e) { - console.error(`${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy root ViewContainerRef`); - } + this._dynamicContainerRef = null; + } - this._appRootViewContainerRef = null; + private _clearAppRootViewContainerRef(): void { + try { + this._appRootViewContainerRef.clear(); + } catch (e) { + console.error( + `${DynamicComponentsService.CLASS_NAME}: Potential bug found, failed to destroy root ViewContainerRef`, + ); } - private static _getOrGenerateUUID(uuid: string): string { - return uuid ?? CollectionsUtil.generateUUID(); - } + this._appRootViewContainerRef = null; + } + + private static _getOrGenerateUUID(uuid: string): string { + return uuid ?? CollectionsUtil.generateUUID(); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/index.ts index a1315e30c8..bf01337e53 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/dynamic-components/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './dynamic-components.service'; +export * from "./dynamic-components.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts index de62709897..cda649c573 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './services'; +export * from "./services"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.spec.ts index c122e4b2f0..52feab1faf 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.spec.ts @@ -5,486 +5,502 @@ /* eslint-disable max-len */ -import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { HttpErrorResponse, HttpHeaders } from "@angular/common/http"; +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; -import { Observable } from 'rxjs'; +import { Observable } from "rxjs"; -import { VmwToastType } from '../../../commons'; +import { VmwToastType } from "../../../commons"; -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { ToastService } from '../../toasts/service'; -import { FormattedError } from '../../toasts/model'; +import { ToastService } from "../../toasts/service"; +import { FormattedError } from "../../toasts/model"; -import { ErrorHandlerService } from './error-handler.service'; +import { ErrorHandlerService } from "./error-handler.service"; -describe('ErrorHandlerService', () => { - let toastServiceStub: jasmine.SpyObj; - let service: ErrorHandlerService; +describe("ErrorHandlerService", () => { + let toastServiceStub: jasmine.SpyObj; + let service: ErrorHandlerService; - beforeEach(() => { - toastServiceStub = jasmine.createSpyObj('toastService', ['show']); + beforeEach(() => { + toastServiceStub = jasmine.createSpyObj("toastService", [ + "show", + ]); - TestBed.configureTestingModule({ - providers: [{ provide: ToastService, useValue: toastServiceStub }, ErrorHandlerService] - }); - - service = TestBed.inject(ErrorHandlerService); + TestBed.configureTestingModule({ + providers: [ + { provide: ToastService, useValue: toastServiceStub }, + ErrorHandlerService, + ], }); - it('should verify instance is created', () => { + service = TestBed.inject(ErrorHandlerService); + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + }); + + describe("Methods::", () => { + describe("|handleError|", () => { + it("should verify will return Observable in error state", fakeAsync(() => { + // Given + const rootError = new Error("some error"); + const processErrorSpy: jasmine.Spy<(error: Error) => void> = spyOn( + service, + "processError", + ); + + // When + const observable$ = service.handleError(rootError); + // Then - expect(service).toBeDefined(); + expect(processErrorSpy).toHaveBeenCalledWith(rootError); + expect(observable$).toBeInstanceOf(Observable); + + observable$.subscribe(CallFake, (error: unknown) => { + expect(error).toEqual(new Error("Something unexpected happened")); + }); + + tick(100); + })); }); - describe('Methods::', () => { - describe('|handleError|', () => { - it('should verify will return Observable in error state', fakeAsync(() => { - // Given - const rootError = new Error('some error'); - const processErrorSpy: jasmine.Spy<(error: Error) => void> = spyOn(service, 'processError'); + describe("|processError|", () => { + let logErrorSpy: jasmine.Spy; + let url: string; + + beforeEach(() => { + logErrorSpy = spyOn(console, "error").and.callFake(CallFake); + url = "/shared/resources/statistics/15"; + }); + + describe("should verify will invoke ToastService with expected Toast:", () => { + it("Error is Nil", () => { + // Given + const rootError: Error = null; + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: `An error occurred: undefined`, + description: + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + error: rootError, + responseStatus: null, + }); + }); - // When - const observable$ = service.handleError(rootError); + it("Error of type SyntaxError", () => { + // Given + const rootError = new SyntaxError( + "Uncaught SyntaxError: Function statements require a function name", + ); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: `An error occurred: ${rootError.message}`, + description: + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + error: rootError, + responseStatus: null, + }); + }); - // Then - expect(processErrorSpy).toHaveBeenCalledWith(rootError); - expect(observable$).toBeInstanceOf(Observable); + it("Error of type SyntaxError and provided additional ErrorHandlerConfig", () => { + // Given + const rootError = new SyntaxError( + "Uncaught SyntaxError: Function statements require a function name", + ); + + // When + service.processError(rootError, { + title: "---> Unknown error happened", + description: + "---> Please try again later, or contact you administrator.", + type: VmwToastType.INFO, + }); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + title: "---> Unknown error happened", + description: + "---> Please try again later, or contact you administrator.", + type: VmwToastType.INFO, + error: rootError, + responseStatus: null, + extendedData: { + title: `An error occurred: ${rootError.message}`, + description: + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + }, + }); + }); + + it("Error of type HttpErrorResponse when sub-error is of type ErrorEvent", () => { + // Given + const topRootError = new ErrorEvent("Connection lost"); + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 0, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith( + `An error occurred: ${(rootError.error as Error).message}`, + ); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: `An error occurred: ${rootError.message}`, + description: + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + error: rootError, + responseStatus: null, + }); + }); + + it("Error of type HttpErrorResponse when status 403 and sub-error is server side error", () => { + // Given + const topRootError = { message: "Access Denied" } as Error; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 403, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(topRootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "ACCESS DENIED", + description: + "You are not authorized for this content! " + + "If you think it is a mistake please contact the data owners and request them to grant you access.", + error: topRootError, + responseStatus: 403, + }); + }); + + it("Error of type HttpErrorResponse when status 500 and sub-error is Nil", () => { + // Given + const topRootError = null; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 500, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Internal Server Error", + description: + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + error: topRootError, + responseStatus: 500, + }); + }); - observable$.subscribe(CallFake, (error: unknown) => { - expect(error).toEqual(new Error('Something unexpected happened')); - }); + it("Error of type HttpErrorResponse when status 500 and sub-error is server side error with what and why", () => { + // Given + const topRootError = { + message: "Internal Server Error", + what: "Server is down, internal server error", + why: "Unknown error happened on server side", + }; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 500, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(topRootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: topRootError.what, + description: topRootError.why, + error: topRootError, + responseStatus: 500, + }); + }); + + it("Error of type HttpErrorResponse when status 500 and sub-error is server side error", () => { + // Given + const topRootError = { + message: "Internal Server Error", + } as Error; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 500, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(topRootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Internal Server Error", + description: + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + error: topRootError, + responseStatus: 500, + }); + }); + + it("Error of type HttpErrorResponse when status 400 and sub-error is Nil", () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 400, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Invalid param", + description: "Operation failed", + error: rootError, + responseStatus: 400, + }); + }); + + it("Error of type HttpErrorResponse when status 401 and sub-error is Nil", () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 401, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Unauthorized", + description: "Operation failed", + error: rootError, + responseStatus: 401, + }); + }); + + it("Error of type HttpErrorResponse when status 404 and sub-error is Nil", () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 404, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Not Found", + description: "Operation failed", + error: rootError, + responseStatus: 404, + }); + }); + + it("Error of type HttpErrorResponse when status 405 and sub-error is Nil", () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 405, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Not Allowed", + description: "Operation failed", + error: rootError, + responseStatus: 405, + }); + }); + + it("Error of type HttpErrorResponse when status 422 and sub-error is Nil", () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 422, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Invalid operation", + description: "Operation failed", + error: rootError, + responseStatus: 422, + }); + }); + + it("Error of type HttpErrorResponse when status 501 and sub-error is Nil", () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 501, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Unknown Error", + description: "Operation failed", + error: rootError, + responseStatus: 501, + }); + }); + + it("Error of type HttpErrorResponse when status 507 and sub-error is server side error with what and why", () => { + // Given + const rootError = new HttpErrorResponse({ + error: { + message: "Insufficient Storage", + what: "Insufficient Storage on server", + why: "Cloud storage id down", + }, + status: 507, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: (rootError.error as FormattedError).what, + description: (rootError.error as FormattedError).why, + error: rootError.error, + responseStatus: 507, + }); + }); - tick(100); - })); + it("Error of type HttpErrorResponse when status 423 and sub-error is of type string", () => { + // Given + const rootError = new HttpErrorResponse({ + error: "Entity is locked", + status: 423, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: rootError.error, + description: rootError.message, + error: rootError.error, + responseStatus: 423, + }); }); - describe('|processError|', () => { - let logErrorSpy: jasmine.Spy; - let url: string; - - beforeEach(() => { - logErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - url = '/shared/resources/statistics/15'; - }); - - describe('should verify will invoke ToastService with expected Toast:', () => { - it('Error is Nil', () => { - // Given - const rootError: Error = null; - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: `An error occurred: undefined`, - description: - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', - error: rootError, - responseStatus: null - }); - }); - - it('Error of type SyntaxError', () => { - // Given - const rootError = new SyntaxError('Uncaught SyntaxError: Function statements require a function name'); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: `An error occurred: ${rootError.message}`, - description: - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', - error: rootError, - responseStatus: null - }); - }); - - it('Error of type SyntaxError and provided additional ErrorHandlerConfig', () => { - // Given - const rootError = new SyntaxError('Uncaught SyntaxError: Function statements require a function name'); - - // When - service.processError(rootError, { - title: '---> Unknown error happened', - description: '---> Please try again later, or contact you administrator.', - type: VmwToastType.INFO - }); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - title: '---> Unknown error happened', - description: '---> Please try again later, or contact you administrator.', - type: VmwToastType.INFO, - error: rootError, - responseStatus: null, - extendedData: { - title: `An error occurred: ${rootError.message}`, - description: - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.' - } - }); - }); - - it('Error of type HttpErrorResponse when sub-error is of type ErrorEvent', () => { - // Given - const topRootError = new ErrorEvent('Connection lost'); - const rootError = new HttpErrorResponse({ - error: topRootError, - status: 0, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(`An error occurred: ${(rootError.error as Error).message}`); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: `An error occurred: ${rootError.message}`, - description: - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', - error: rootError, - responseStatus: null - }); - }); - - it('Error of type HttpErrorResponse when status 403 and sub-error is server side error', () => { - // Given - const topRootError = { message: 'Access Denied' } as Error; - const rootError = new HttpErrorResponse({ - error: topRootError, - status: 403, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(topRootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'ACCESS DENIED', - description: - 'You are not authorized for this content! ' + - 'If you think it is a mistake please contact the data owners and request them to grant you access.', - error: topRootError, - responseStatus: 403 - }); - }); - - it('Error of type HttpErrorResponse when status 500 and sub-error is Nil', () => { - // Given - const topRootError = null; - const rootError = new HttpErrorResponse({ - error: topRootError, - status: 500, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Internal Server Error', - description: - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', - error: topRootError, - responseStatus: 500 - }); - }); - - it('Error of type HttpErrorResponse when status 500 and sub-error is server side error with what and why', () => { - // Given - const topRootError = { - message: 'Internal Server Error', - what: 'Server is down, internal server error', - why: 'Unknown error happened on server side' - }; - const rootError = new HttpErrorResponse({ - error: topRootError, - status: 500, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(topRootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: topRootError.what, - description: topRootError.why, - error: topRootError, - responseStatus: 500 - }); - }); - - it('Error of type HttpErrorResponse when status 500 and sub-error is server side error', () => { - // Given - const topRootError = { - message: 'Internal Server Error' - } as Error; - const rootError = new HttpErrorResponse({ - error: topRootError, - status: 500, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(topRootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Internal Server Error', - description: - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', - error: topRootError, - responseStatus: 500 - }); - }); - - it('Error of type HttpErrorResponse when status 400 and sub-error is Nil', () => { - // Given - const rootError = new HttpErrorResponse({ - error: null, - status: 400, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Invalid param', - description: 'Operation failed', - error: rootError, - responseStatus: 400 - }); - }); - - it('Error of type HttpErrorResponse when status 401 and sub-error is Nil', () => { - // Given - const rootError = new HttpErrorResponse({ - error: null, - status: 401, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Unauthorized', - description: 'Operation failed', - error: rootError, - responseStatus: 401 - }); - }); - - it('Error of type HttpErrorResponse when status 404 and sub-error is Nil', () => { - // Given - const rootError = new HttpErrorResponse({ - error: null, - status: 404, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Not Found', - description: 'Operation failed', - error: rootError, - responseStatus: 404 - }); - }); - - it('Error of type HttpErrorResponse when status 405 and sub-error is Nil', () => { - // Given - const rootError = new HttpErrorResponse({ - error: null, - status: 405, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Not Allowed', - description: 'Operation failed', - error: rootError, - responseStatus: 405 - }); - }); - - it('Error of type HttpErrorResponse when status 422 and sub-error is Nil', () => { - // Given - const rootError = new HttpErrorResponse({ - error: null, - status: 422, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Invalid operation', - description: 'Operation failed', - error: rootError, - responseStatus: 422 - }); - }); - - it('Error of type HttpErrorResponse when status 501 and sub-error is Nil', () => { - // Given - const rootError = new HttpErrorResponse({ - error: null, - status: 501, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Unknown Error', - description: 'Operation failed', - error: rootError, - responseStatus: 501 - }); - }); - - it('Error of type HttpErrorResponse when status 507 and sub-error is server side error with what and why', () => { - // Given - const rootError = new HttpErrorResponse({ - error: { - message: 'Insufficient Storage', - what: 'Insufficient Storage on server', - why: 'Cloud storage id down' - }, - status: 507, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: (rootError.error as FormattedError).what, - description: (rootError.error as FormattedError).why, - error: rootError.error, - responseStatus: 507 - }); - }); - - it('Error of type HttpErrorResponse when status 423 and sub-error is of type string', () => { - // Given - const rootError = new HttpErrorResponse({ - error: 'Entity is locked', - status: 423, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: rootError.error, - description: rootError.message, - error: rootError.error, - responseStatus: 423 - }); - }); - - it('Error of type HttpErrorResponse when status 417 and sub-error is server side error', () => { - // Given - const rootError = new HttpErrorResponse({ - error: { - message: 'Expectation Failed' - }, - status: 417, - url, - headers: new HttpHeaders() - }); - - // When - service.processError(rootError); - - // Then - expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); - expect(toastServiceStub.show).toHaveBeenCalledWith({ - type: VmwToastType.FAILURE, - title: 'Unknown Error', - description: rootError.message, - error: rootError.error, - responseStatus: 417 - }); - }); - }); + it("Error of type HttpErrorResponse when status 417 and sub-error is server side error", () => { + // Given + const rootError = new HttpErrorResponse({ + error: { + message: "Expectation Failed", + }, + status: 417, + url, + headers: new HttpHeaders(), + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: "Unknown Error", + description: rootError.message, + error: rootError.error, + responseStatus: 417, + }); }); + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.ts index 8e1e1c9c28..0dbede8226 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/error-handler.service.ts @@ -5,36 +5,36 @@ /* eslint-disable @typescript-eslint/unified-signatures */ -import { Injectable } from '@angular/core'; -import { HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from "@angular/core"; +import { HttpErrorResponse } from "@angular/common/http"; -import { Observable, throwError } from 'rxjs'; +import { Observable, throwError } from "rxjs"; -import { VmwToastType } from '../../../commons'; +import { VmwToastType } from "../../../commons"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { FormattedError, Toast } from '../../toasts/model'; -import { ToastService } from '../../toasts/service'; +import { FormattedError, Toast } from "../../toasts/model"; +import { ToastService } from "../../toasts/service"; /** * ** Config for Toast message. */ export interface ErrorHandlerConfig { - /** - * ** Toast title. - */ - title?: Toast['title']; - - /** - * ** Toast description. - */ - description?: Toast['description']; - - /** - * ** Toast type. - */ - type?: Toast['type']; + /** + * ** Toast title. + */ + title?: Toast["title"]; + + /** + * ** Toast description. + */ + description?: Toast["description"]; + + /** + * ** Toast type. + */ + type?: Toast["type"]; } /** @@ -42,152 +42,180 @@ export interface ErrorHandlerConfig { */ @Injectable() export class ErrorHandlerService { - /** - * ** Constructor. - */ - constructor(private readonly toastService: ToastService) {} - - /** - * ** Handle Error in rxjs stream. - * - * - Show Toast message - * - Log it to console - * - Re-throw new Error('Something unexpected happened') - */ - handleError = (error: Error): Observable => { - this.processError(error); - - const newError = new Error('Something unexpected happened'); - - return throwError(() => newError); - }; + /** + * ** Constructor. + */ + constructor(private readonly toastService: ToastService) {} + + /** + * ** Handle Error in rxjs stream. + * + * - Show Toast message + * - Log it to console + * - Re-throw new Error('Something unexpected happened') + */ + handleError = (error: Error): Observable => { + this.processError(error); + + const newError = new Error("Something unexpected happened"); + + return throwError(() => newError); + }; + + /** + * ** Process Error. + * + * - Show Toast message + * - Log it to console + */ + processError(error: Error): void; + processError(error: Error, overriddenConfig: ErrorHandlerConfig): void; + processError(error: Error, overriddenConfig?: ErrorHandlerConfig): void { + if (error instanceof HttpErrorResponse) { + if (error.error instanceof ErrorEvent) { + // A client-side or network error occurred. + const toast = ErrorHandlerService._createToastConfigForError( + error, + overriddenConfig, + ); - /** - * ** Process Error. - * - * - Show Toast message - * - Log it to console - */ - processError(error: Error): void; - processError(error: Error, overriddenConfig: ErrorHandlerConfig): void; - processError(error: Error, overriddenConfig?: ErrorHandlerConfig): void { - if (error instanceof HttpErrorResponse) { - if (error.error instanceof ErrorEvent) { - // A client-side or network error occurred. - const toast = ErrorHandlerService._createToastConfigForError(error, overriddenConfig); - - console.error(`An error occurred: ${error.error.message}`); - - this.toastService.show(toast); - } else { - // Server side error occurred. - const toast = ErrorHandlerService._createToastConfigForHttpErrorResponse(error, overriddenConfig); - - console.error(error.error ?? error); - - this.toastService.show(toast); - } - } else { - // Runtime error occurred, potential bug. - const toast = ErrorHandlerService._createToastConfigForError(error, overriddenConfig); - - console.error(error); - - this.toastService.show(toast); - } - } + console.error(`An error occurred: ${error.error.message}`); - /* eslint-disable @typescript-eslint/member-ordering */ - - private static _createToastConfigForHttpErrorResponse(error: HttpErrorResponse, overriddenConfig: ErrorHandlerConfig): Toast { - let title: string; - let description: string; - let rootError: Error = error.error as Error; - const responseStatus = error.status; - - if (error.status === 403) { - title = 'ACCESS DENIED'; - description = - 'You are not authorized for this content! ' + - 'If you think it is a mistake please contact the data owners and request them to grant you access.'; - } else if (error.status === 500) { - title = (error.error as FormattedError)?.what ? (error.error as FormattedError).what : 'Internal Server Error'; - description = (error.error as FormattedError)?.why - ? (error.error as FormattedError).why - : 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.'; - } else if (CollectionsUtil.isNil(error.error)) { - title = ErrorHandlerService._getErrorTitle(error.status); - description = 'Operation failed'; - rootError = error; - } else if (error.error && (error.error as FormattedError).what && (error.error as FormattedError).why) { - title = (error.error as FormattedError).what; - description = (error.error as FormattedError).why; - } else if (typeof error.error === 'string') { - title = error.error; - description = error.message; - } else { - title = ErrorHandlerService._getErrorTitle(error.status); - description = error.message; - } - - return ErrorHandlerService._createToastConfig(title, description, rootError, responseStatus, overriddenConfig); - } - - private static _createToastConfigForError(error: Error, overriddenConfig: ErrorHandlerConfig): Toast { - return ErrorHandlerService._createToastConfig( - `An error occurred: ${error?.message}`, - 'We are sorry for the inconvenience.' + - 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + this.toastService.show(toast); + } else { + // Server side error occurred. + const toast = + ErrorHandlerService._createToastConfigForHttpErrorResponse( error, - undefined, - overriddenConfig - ); + overriddenConfig, + ); + + console.error(error.error ?? error); + + this.toastService.show(toast); + } + } else { + // Runtime error occurred, potential bug. + const toast = ErrorHandlerService._createToastConfigForError( + error, + overriddenConfig, + ); + + console.error(error); + + this.toastService.show(toast); + } + } + + /* eslint-disable @typescript-eslint/member-ordering */ + + private static _createToastConfigForHttpErrorResponse( + error: HttpErrorResponse, + overriddenConfig: ErrorHandlerConfig, + ): Toast { + let title: string; + let description: string; + let rootError: Error = error.error as Error; + const responseStatus = error.status; + + if (error.status === 403) { + title = "ACCESS DENIED"; + description = + "You are not authorized for this content! " + + "If you think it is a mistake please contact the data owners and request them to grant you access."; + } else if (error.status === 500) { + title = (error.error as FormattedError)?.what + ? (error.error as FormattedError).what + : "Internal Server Error"; + description = (error.error as FormattedError)?.why + ? (error.error as FormattedError).why + : "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error."; + } else if (CollectionsUtil.isNil(error.error)) { + title = ErrorHandlerService._getErrorTitle(error.status); + description = "Operation failed"; + rootError = error; + } else if ( + error.error && + (error.error as FormattedError).what && + (error.error as FormattedError).why + ) { + title = (error.error as FormattedError).what; + description = (error.error as FormattedError).why; + } else if (typeof error.error === "string") { + title = error.error; + description = error.message; + } else { + title = ErrorHandlerService._getErrorTitle(error.status); + description = error.message; } - private static _getErrorTitle(status: number): string { - switch (status) { - case 400: - return 'Invalid param'; - case 401: - return 'Unauthorized'; - case 404: - return 'Not Found'; - case 405: - return 'Not Allowed'; - case 422: - return 'Invalid operation'; - default: - return 'Unknown Error'; - } + return ErrorHandlerService._createToastConfig( + title, + description, + rootError, + responseStatus, + overriddenConfig, + ); + } + + private static _createToastConfigForError( + error: Error, + overriddenConfig: ErrorHandlerConfig, + ): Toast { + return ErrorHandlerService._createToastConfig( + `An error occurred: ${error?.message}`, + "We are sorry for the inconvenience." + + "Please try again or come back later, and if the issue persists – please copy the details and report the error.", + error, + undefined, + overriddenConfig, + ); + } + + private static _getErrorTitle(status: number): string { + switch (status) { + case 400: + return "Invalid param"; + case 401: + return "Unauthorized"; + case 404: + return "Not Found"; + case 405: + return "Not Allowed"; + case 422: + return "Invalid operation"; + default: + return "Unknown Error"; } + } + + private static _createToastConfig( + title: string, + description: string, + error: Error, + responseStatus: number = null, + overriddenConfig: ErrorHandlerConfig = null, + ): Toast { + let toastConfig: Toast = { + title, + description, + type: VmwToastType.FAILURE, + error, + responseStatus, + }; - private static _createToastConfig( - title: string, - description: string, - error: Error, - responseStatus: number = null, - overriddenConfig: ErrorHandlerConfig = null - ): Toast { - let toastConfig: Toast = { - title, - description, - type: VmwToastType.FAILURE, - error, - responseStatus - }; - - if (CollectionsUtil.isDefined(overriddenConfig)) { - toastConfig = { - ...toastConfig, - ...overriddenConfig, - extendedData: { - title, - description - } - }; - } - - return toastConfig; + if (CollectionsUtil.isDefined(overriddenConfig)) { + toastConfig = { + ...toastConfig, + ...overriddenConfig, + extendedData: { + title, + description, + }, + }; } + + return toastConfig; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/index.ts index 823dd83e89..59d0b38a4f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './error-handler.service'; +export * from "./error-handler.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/pipes.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/pipes.module.ts index 1612f6c989..05c58af91b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/pipes.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/pipes.module.ts @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule } from "@angular/router"; @NgModule({ - imports: [CommonModule, RouterModule], - declarations: [], - exports: [] + imports: [CommonModule, RouterModule], + declarations: [], + exports: [], }) export class PipesModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/public-api.ts index 6bbb3c2e7d..d4da7b6335 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/pipes/public-api.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './pipes.module'; +export * from "./pipes.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.html index 8fc0d181db..b0c5fee515 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.html @@ -4,75 +4,75 @@ --> - + - - + + - - -

    - {{title}} -

    -
    - -

    - {{title}} -

    -
    - -

    - {{title}} -

    -
    - -

    - {{title}} -

    -
    - -
    - {{title}} -
    -
    - -
    - {{title}} -
    -
    + + +

    + {{ title }} +

    + +

    + {{ title }} +

    +
    + +

    + {{ title }} +

    +
    + +

    + {{ title }} +

    +
    + +
    + {{ title }} +
    +
    + +
    + {{ title }} +
    +
    +
    -
    - {{description}} -
    +
    + {{ description }} +
    - +
    - + diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.scss index c737217fe2..0f35abce77 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.scss @@ -7,43 +7,43 @@ $text-dark: #919fa8; $text-light: #67747d; :host { - display: flex; - align-items: center; - flex-direction: column; - margin: 1rem 0; - - clr-icon { - fill: #ccc; - } - - .empty-placeholder-heading { - font-size: 18px; - color: $text-light; - margin-top: 18px; - line-height: 48px; - font-weight: 200; - } - - .empty-placeholder-description { - margin-top: 0; - font-size: 13px; - color: $text-light; - margin-bottom: 18px; - font-weight: 200; - } + display: flex; + align-items: center; + flex-direction: column; + margin: 1rem 0; + + clr-icon { + fill: #ccc; + } + + .empty-placeholder-heading { + font-size: 18px; + color: $text-light; + margin-top: 18px; + line-height: 48px; + font-weight: 200; + } + + .empty-placeholder-description { + margin-top: 0; + font-size: 13px; + color: $text-light; + margin-bottom: 18px; + font-weight: 200; + } } :host ::ng-deep button:last-child { - margin-right: 0 !important; + margin-right: 0 !important; } :host ::ng-deep button.btn-link { - margin-top: 0 !important; + margin-top: 0 !important; } :host-context(.dark) { - h2, - h3 { - color: $text-dark; - } + h2, + h3 { + color: $text-dark; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.spec.ts index 5e9470a429..1a65b13357 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.spec.ts @@ -3,27 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { EmptyStateComponent } from './empty-state.component'; +import { EmptyStateComponent } from "./empty-state.component"; -describe('EmptyStateComponent', () => { - let component: EmptyStateComponent; - let fixture: ComponentFixture; +describe("EmptyStateComponent", () => { + let component: EmptyStateComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [EmptyStateComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [EmptyStateComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(EmptyStateComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.ts index 166bf881ed..ac91ab80c1 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/empty-state.component.ts @@ -3,33 +3,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; +import { Component, ContentChild, Input, TemplateRef } from "@angular/core"; @Component({ - selector: 'shared-empty-state', - templateUrl: './empty-state.component.html', - styleUrls: ['./empty-state.component.scss'] + selector: "shared-empty-state", + templateUrl: "./empty-state.component.html", + styleUrls: ["./empty-state.component.scss"], }) export class EmptyStateComponent { - @ContentChild('customTemplate', { read: TemplateRef }) customTemplateRef: TemplateRef; + @ContentChild("customTemplate", { read: TemplateRef }) + customTemplateRef: TemplateRef; - /** - * ** Title for empty state Component. - */ - @Input() title: string; + /** + * ** Title for empty state Component. + */ + @Input() title: string; - /** - * ** Icon for empty state Component. - */ - @Input() icon: string; + /** + * ** Icon for empty state Component. + */ + @Input() icon: string; - /** - * ** Description for empty state Component. - */ - @Input() description: string; + /** + * ** Description for empty state Component. + */ + @Input() description: string; - /** - * ** Title heading level for empty state Component. - */ - @Input() headingLevel = 2; + /** + * ** Title heading level for empty state Component. + */ + @Input() headingLevel = 2; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/index.ts index f914b7eb01..9d85b2ca72 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/empty-state/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './empty-state.component'; +export * from "./empty-state.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/index.ts index 8cec81a589..b4cf785401 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './empty-state'; -export * from './placeholder.component'; +export * from "./empty-state"; +export * from "./placeholder.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.html index 3593d39b0a..186ee5174d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.html @@ -6,1193 +6,1183 @@ - - -
    - - - - - - - - - - - - - -
    -
    - - - - - - + + +
    + + + + + + - - - - - - Empty state icon - - + + + + + +
    +
    - - {{ emptyMessage }} - + + + + + + + + + + + + Empty state icon + + + + + {{ emptyMessage }} + +
    -
    - Error state icon +
    + Error state icon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

    {{ error.problem }}

    -

    - {{ error.description }} - - {{ error.mitigation }} - - -
    - -
    -

    -

    - Impacted services: {{ error.impactedServices }} -

    -
    +

    {{ error.problem }}

    +

    + {{ error.description }} + + {{ error.mitigation }} + + +
    + +
    +

    +

    + Impacted services: {{ error.impactedServices }} +

    +
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.scss index fd4ab4d624..36954f597f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.scss @@ -7,81 +7,81 @@ $text-dark: var(--clr-h2-color, black); $text-light: #eaedf0; :host { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .shared__placeholder { - ::ng-deep shared-empty-state { - margin-top: 3rem; - margin-bottom: 20px; + ::ng-deep shared-empty-state { + margin-top: 3rem; + margin-bottom: 20px; - .empty-placeholder-heading { - line-height: 30px; - margin-bottom: 18px; - } + .empty-placeholder-heading { + line-height: 30px; + margin-bottom: 18px; + } - .empty-placeholder-description { - white-space: pre-wrap; - text-align: center; - } + .empty-placeholder-description { + white-space: pre-wrap; + text-align: center; } + } - .shared__placeholder-error-container { - &:not(.shared__placeholder-error-container--system-default) { - display: flex; - flex-direction: column; - align-items: center; - margin: 24px 0; - } + .shared__placeholder-error-container { + &:not(.shared__placeholder-error-container--system-default) { + display: flex; + flex-direction: column; + align-items: center; + margin: 24px 0; } + } - .shared__placeholder-error-template--system-default, - ::ng-deep .shared__placeholder-error-template--custom { - display: flex; - flex-direction: column; - align-items: center; - margin: 1rem 0; + .shared__placeholder-error-template--system-default, + ::ng-deep .shared__placeholder-error-template--custom { + display: flex; + flex-direction: column; + align-items: center; + margin: 1rem 0; - h2 { - font-size: 18px; - line-height: 24px; - text-align: center; - color: $text-dark; - margin: 24px 0 18px 0; - } + h2 { + font-size: 18px; + line-height: 24px; + text-align: center; + color: $text-dark; + margin: 24px 0 18px 0; + } - h3 { - font-size: 14px; - line-height: 24px; - text-align: center; - color: $text-dark; - margin: 0 0 12px 0; + h3 { + font-size: 14px; + line-height: 24px; + text-align: center; + color: $text-dark; + margin: 0 0 12px 0; - .shared__placeholder-error-template-escalation { - ::ng-deep a { - text-decoration: underline !important; - } - } + .shared__placeholder-error-template-escalation { + ::ng-deep a { + text-decoration: underline !important; } + } + } - h4 { - font-size: 13px; - line-height: 24px; - text-align: center; - color: $text-dark; - margin: 0; - } + h4 { + font-size: 13px; + line-height: 24px; + text-align: center; + color: $text-dark; + margin: 0; } + } } :host-context(.dark) { - .shared__placeholder-error-template--system-default, - ::ng-deep .shared__placeholder-error-template--custom { - h2, - h3, - h4 { - color: $text-light; - } + .shared__placeholder-error-template--system-default, + ::ng-deep .shared__placeholder-error-template--custom { + h2, + h3, + h4 { + color: $text-light; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.spec.ts index f1a793cf5f..609ec264b8 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.spec.ts @@ -3,55 +3,58 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA, ElementRef, Renderer2 } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SHARED_FEATURES_CONFIG_TOKEN } from '../../_token'; - -import { PlaceholderConfig } from '../model'; - -import { PlaceholderComponent } from './placeholder.component'; - -describe('PlaceholderComponent', () => { - let serviceRequestUrl: string; - let elementRefFake: ElementRef; - let renderer2Stub: jasmine.SpyObj; - - let component: PlaceholderComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - elementRefFake = { - nativeElement: null - }; - renderer2Stub = jasmine.createSpyObj('renderer2Stub', [ - 'removeChild', - 'parentNode', - 'createElement', - 'setAttribute', - 'appendChild' - ]); - - await TestBed.configureTestingModule({ - declarations: [PlaceholderComponent], - providers: [ - { provide: ElementRef, useValue: elementRefFake }, - { provide: Renderer2, useValue: renderer2Stub }, - { provide: SHARED_FEATURES_CONFIG_TOKEN, useValue: { placeholder: { serviceRequestUrl } } as PlaceholderConfig } - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); - }); - - beforeEach(() => { - serviceRequestUrl = 'https://service-url'; - - fixture = TestBed.createComponent(PlaceholderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); +import { CUSTOM_ELEMENTS_SCHEMA, ElementRef, Renderer2 } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SHARED_FEATURES_CONFIG_TOKEN } from "../../_token"; + +import { PlaceholderConfig } from "../model"; + +import { PlaceholderComponent } from "./placeholder.component"; + +describe("PlaceholderComponent", () => { + let serviceRequestUrl: string; + let elementRefFake: ElementRef; + let renderer2Stub: jasmine.SpyObj; + + let component: PlaceholderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + elementRefFake = { + nativeElement: null, + }; + renderer2Stub = jasmine.createSpyObj("renderer2Stub", [ + "removeChild", + "parentNode", + "createElement", + "setAttribute", + "appendChild", + ]); + + await TestBed.configureTestingModule({ + declarations: [PlaceholderComponent], + providers: [ + { provide: ElementRef, useValue: elementRefFake }, + { provide: Renderer2, useValue: renderer2Stub }, + { + provide: SHARED_FEATURES_CONFIG_TOKEN, + useValue: { placeholder: { serviceRequestUrl } } as PlaceholderConfig, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + serviceRequestUrl = "https://service-url"; + + fixture = TestBed.createComponent(PlaceholderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.ts index c9c7b710fa..6f208615b3 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/components/placeholder.component.ts @@ -4,119 +4,119 @@ */ import { - Component, - ContentChild, - ElementRef, - Inject, - Input, - OnChanges, - OnInit, - Renderer2, - SimpleChanges, - TemplateRef, - ViewChild -} from '@angular/core'; -import { HttpStatusCode } from '@angular/common/http'; + Component, + ContentChild, + ElementRef, + Inject, + Input, + OnChanges, + OnInit, + Renderer2, + SimpleChanges, + TemplateRef, + ViewChild, +} from "@angular/core"; +import { HttpStatusCode } from "@angular/common/http"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { ApiErrorMessage, ErrorRecord, TaurusObject } from '../../../common'; +import { ApiErrorMessage, ErrorRecord, TaurusObject } from "../../../common"; -import { filterErrorRecords } from '../../../core'; +import { filterErrorRecords } from "../../../core"; -import { SHARED_FEATURES_CONFIG_TOKEN } from '../../_token'; +import { SHARED_FEATURES_CONFIG_TOKEN } from "../../_token"; -import { PlaceholderConfig } from '../model'; +import { PlaceholderConfig } from "../model"; -import { PlaceholderService } from '../services'; +import { PlaceholderService } from "../services"; /* eslint-disable @typescript-eslint/naming-convention */ interface PlaceholderAutoSupportedStates { - /** - * ** Fallback state. - */ - Generic: string; - /** - * ** When there is no internet connection (offline). - */ - Offline: string; - /** - * ** When asset/entity is not found. - */ - NotFound: string; + /** + * ** Fallback state. + */ + Generic: string; + /** + * ** When there is no internet connection (offline). + */ + Offline: string; + /** + * ** When asset/entity is not found. + */ + NotFound: string; } // empty state const EmptyMessage = { - Generic: 'No assets found!' + Generic: "No assets found!", }; const EmptyImgSource = { - Generic: 'assets/images/empty/empty-generic.svg' + Generic: "assets/images/empty/empty-generic.svg", }; const EmptyImgStyle = { - Opacity: 1, - Width: '64px' + Opacity: 1, + Width: "64px", }; // error state const ErrorProblem = { - Generic: '%s %s currently unavailable', - Offline: 'No Internet Connection', - NotFound: '%s %s not found' + Generic: "%s %s currently unavailable", + Offline: "No Internet Connection", + NotFound: "%s %s not found", }; const ErrorDescription = { - Generic: '%s can not be loaded, due to technical error on our end.', - Offline: 'Application recorded network outage.', - NotFound: `%s for requested identifier does not exist in the system.` + Generic: "%s can not be loaded, due to technical error on our end.", + Offline: "Application recorded network outage.", + NotFound: `%s for requested identifier does not exist in the system.`, }; const ErrorMitigation: PlaceholderAutoSupportedStates = { - Generic: 'Please try again later.', - Offline: 'Please check your internet connection.', - NotFound: '' + Generic: "Please try again later.", + Offline: "Please check your internet connection.", + NotFound: "", }; const ErrorEscalation: PlaceholderAutoSupportedStates = { - // anchor href interpolated in Component constructor with provided config - Generic: `If the issue persists, please open a service request.`, - Offline: '', - // anchor href interpolated in Component constructor with provided config - NotFound: `If you think it is a bug, please open a service request.` + // anchor href interpolated in Component constructor with provided config + Generic: `If the issue persists, please open a service request.`, + Offline: "", + // anchor href interpolated in Component constructor with provided config + NotFound: `If you think it is a bug, please open a service request.`, }; const ErrorImgSource: PlaceholderAutoSupportedStates = { - Generic: 'assets/images/placeholder/server-error.svg', - Offline: '', - NotFound: 'assets/images/placeholder/not-found.svg' + Generic: "assets/images/placeholder/server-error.svg", + Offline: "", + NotFound: "assets/images/placeholder/not-found.svg", }; const ErrorImgStyle: { - Opacity: number; - Width: PlaceholderAutoSupportedStates; + Opacity: number; + Width: PlaceholderAutoSupportedStates; } = { - Opacity: 1, - Width: { - Generic: '200px', - Offline: '280px', - NotFound: '150px' - } + Opacity: 1, + Width: { + Generic: "200px", + Offline: "280px", + NotFound: "150px", + }, }; interface IdentifiedErrorRecordWithMessage { - record: ErrorRecord[]; - problem: string; - description: string; - mitigation: string; - escalation: string; - impactedServices: string; - apiMessage: ApiErrorMessage; - imageSrc: string; - imageWidth: string; - imageOpacity: number; + record: ErrorRecord[]; + problem: string; + description: string; + mitigation: string; + escalation: string; + impactedServices: string; + apiMessage: ApiErrorMessage; + imageSrc: string; + imageWidth: string; + imageOpacity: number; } /* eslint-enable @typescript-eslint/naming-convention */ @@ -128,689 +128,799 @@ interface IdentifiedErrorRecordWithMessage { * - Handles empty state and error state according provided parameters (instructions). */ @Component({ - selector: 'shared-placeholder', - templateUrl: './placeholder.component.html', - styleUrls: ['./placeholder.component.scss'], - providers: [PlaceholderService] + selector: "shared-placeholder", + templateUrl: "./placeholder.component.html", + styleUrls: ["./placeholder.component.scss"], + providers: [PlaceholderService], }) -export class PlaceholderComponent extends TaurusObject implements OnInit, OnChanges { - /** - * ** Template ref for system default Error Template. - * - * - Fallback for any Error that doesn't match any of provided custom Error Templates. - */ - @ViewChild('errorTemplateSystemDefault', { read: TemplateRef }) errorTemplateRefSystemDefault: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate. - * - * - Template is generic and if provided will be used for every error that doesn't match any other template. - * - If not provided will execute next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate', { read: TemplateRef }) errorTemplateRefGeneric: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate4xx. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 4xx. - * - If not provided will execute next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate4xx', { read: TemplateRef }) errorTemplateRefClientErrors: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate400. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 400. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate400', { read: TemplateRef }) errorTemplateRefBadRequest: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate401. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 401. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate401', { read: TemplateRef }) errorTemplateRefUnauthorized: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate403. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 403. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate403', { read: TemplateRef }) errorTemplateRefForbidden: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate404. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 404. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate404', { read: TemplateRef }) errorTemplateRefNotFound: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate405. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 405. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate405', { read: TemplateRef }) errorTemplateRefMethodNotAllowed: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate409. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 409. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate409', { read: TemplateRef }) errorTemplateRefConflict: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate422. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 422. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate422', { read: TemplateRef }) errorTemplateRefUnprocessableEntity: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate5xx. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 5xx. - * - If not provided will execute next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate5xx', { read: TemplateRef }) errorTemplateRefServerErrors: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate500. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 500. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 5xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate500', { read: TemplateRef }) errorTemplateRefInternalServerError: TemplateRef; - - /** - * ** Content projection child query for custom Error Template with ID #errorTemplate503. - * - * - Template if provided will be use only for errors that are HttpErrorResponse with status 503. - * - If not provided will execute next resolution - * - try fallback to custom Error Template for HttpStatusCodes 5xx if found, if not go to next resolution - * - try fallback to generic custom Error Template if found, if not go to next resolution - * - fallback to system default Error Template. - */ - @ContentChild('errorTemplate503', { read: TemplateRef }) errorTemplateRefServiceUnavailable: TemplateRef; - - /** - * ** Content projection child query for custom Empty Template with ID #emptyTemplate. - * - * - Template if provided will be use for empty state otherwise fallback to system default Empty State Template. - */ - @ContentChild('emptyTemplate', { read: TemplateRef }) emptyTemplateRef: TemplateRef; - - /** - * ** Boolean flag that identifies if parent is loading data. - */ - @Input() loading: boolean; - - // empty state - - /** - * ** Text for empty state, if component is rendered without errors or there is no listened error code(s). - */ - @Input() set emptyMessage(value: string) { - if (CollectionsUtil.isString(value) && value.length > 0) { - this._isEmptyMessageExternal = true; - this._emptyMessage = value; - } else { - this._isEmptyMessageExternal = false; - this._emptyMessage = EmptyMessage.Generic; - } +export class PlaceholderComponent + extends TaurusObject + implements OnInit, OnChanges +{ + /** + * ** Template ref for system default Error Template. + * + * - Fallback for any Error that doesn't match any of provided custom Error Templates. + */ + @ViewChild("errorTemplateSystemDefault", { read: TemplateRef }) + errorTemplateRefSystemDefault: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate. + * + * - Template is generic and if provided will be used for every error that doesn't match any other template. + * - If not provided will execute next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate", { read: TemplateRef }) + errorTemplateRefGeneric: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate4xx. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 4xx. + * - If not provided will execute next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate4xx", { read: TemplateRef }) + errorTemplateRefClientErrors: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate400. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 400. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate400", { read: TemplateRef }) + errorTemplateRefBadRequest: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate401. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 401. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate401", { read: TemplateRef }) + errorTemplateRefUnauthorized: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate403. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 403. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate403", { read: TemplateRef }) + errorTemplateRefForbidden: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate404. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 404. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate404", { read: TemplateRef }) + errorTemplateRefNotFound: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate405. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 405. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate405", { read: TemplateRef }) + errorTemplateRefMethodNotAllowed: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate409. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 409. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate409", { read: TemplateRef }) + errorTemplateRefConflict: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate422. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 422. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 4xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate422", { read: TemplateRef }) + errorTemplateRefUnprocessableEntity: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate5xx. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 5xx. + * - If not provided will execute next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate5xx", { read: TemplateRef }) + errorTemplateRefServerErrors: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate500. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 500. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 5xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate500", { read: TemplateRef }) + errorTemplateRefInternalServerError: TemplateRef; + + /** + * ** Content projection child query for custom Error Template with ID #errorTemplate503. + * + * - Template if provided will be use only for errors that are HttpErrorResponse with status 503. + * - If not provided will execute next resolution + * - try fallback to custom Error Template for HttpStatusCodes 5xx if found, if not go to next resolution + * - try fallback to generic custom Error Template if found, if not go to next resolution + * - fallback to system default Error Template. + */ + @ContentChild("errorTemplate503", { read: TemplateRef }) + errorTemplateRefServiceUnavailable: TemplateRef; + + /** + * ** Content projection child query for custom Empty Template with ID #emptyTemplate. + * + * - Template if provided will be use for empty state otherwise fallback to system default Empty State Template. + */ + @ContentChild("emptyTemplate", { read: TemplateRef }) + emptyTemplateRef: TemplateRef; + + /** + * ** Boolean flag that identifies if parent is loading data. + */ + @Input() loading: boolean; + + // empty state + + /** + * ** Text for empty state, if component is rendered without errors or there is no listened error code(s). + */ + @Input() set emptyMessage(value: string) { + if (CollectionsUtil.isString(value) && value.length > 0) { + this._isEmptyMessageExternal = true; + this._emptyMessage = value; + } else { + this._isEmptyMessageExternal = false; + this._emptyMessage = EmptyMessage.Generic; } - - /** - * ** Text for empty state. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - get emptyMessage(): string { - return this._emptyMessage; + } + + /** + * ** Text for empty state. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + get emptyMessage(): string { + return this._emptyMessage; + } + + /** + * ** Empty state image source url. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + @Input() set emptyImgSrc(value: string) { + if (CollectionsUtil.isString(value) && value.length > 0) { + this._isEmptyImgSrcExternal = true; + this._emptyImgSrc = value; + } else { + this._isEmptyImgSrcExternal = false; + this._emptyImgSrc = EmptyImgSource.Generic; } - - /** - * ** Empty state image source url. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - @Input() set emptyImgSrc(value: string) { - if (CollectionsUtil.isString(value) && value.length > 0) { - this._isEmptyImgSrcExternal = true; - this._emptyImgSrc = value; - } else { - this._isEmptyImgSrcExternal = false; - this._emptyImgSrc = EmptyImgSource.Generic; - } + } + + /** + * ** Empty state image source url. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + get emptyImgSrc(): string { + return this._emptyImgSrc; + } + + /** + * ** Empty state image width. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + @Input() set emptyImgWidth(value: string) { + if (CollectionsUtil.isString(value) && value.length > 0) { + this._emptyImgWidth = value; + } else { + this._emptyImgWidth = EmptyImgStyle.Width; } - - /** - * ** Empty state image source url. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - get emptyImgSrc(): string { - return this._emptyImgSrc; + } + + /** + * ** Empty state image width. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + get emptyImgWidth(): string { + return this._emptyImgWidth; + } + + /** + * ** Empty state image opacity. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + @Input() set emptyImgOpacity(value: number) { + if (CollectionsUtil.isNumber(value) && value >= 1 && value <= 1) { + this._emptyImgOpacity = value; + } else { + this._emptyImgOpacity = EmptyImgStyle.Opacity; } - - /** - * ** Empty state image width. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - @Input() set emptyImgWidth(value: string) { - if (CollectionsUtil.isString(value) && value.length > 0) { - this._emptyImgWidth = value; - } else { - this._emptyImgWidth = EmptyImgStyle.Width; - } + } + + /** + * ** Empty state image opacity. + * + * - Visualized only if component is rendered without errors or there is no listened error code(s). + */ + get emptyImgOpacity(): number { + return this._emptyImgOpacity; + } + + /** + * ** Flag to show or hide custom empty state image. + * + * - default value is FALSE. + */ + @Input() showCustomEmptyStateImage = false; + + /** + * ** Flag to show or hide default empty state image in grid. + * + * - default value is FALSE. + */ + @Input() hideDefaultEmptyStateImageInGrid = false; + + // error state + + /** + * ** Errors queue of ErrorRecords injected from parents transitive. + */ + @Input() set errorsQueue(value: ErrorRecord[]) { + if (CollectionsUtil.isArray(value)) { + this._errorsQueue = value; + } else { + this._errorsQueue = []; } - - /** - * ** Empty state image width. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - get emptyImgWidth(): string { - return this._emptyImgWidth; + } + + /** + * ** Errors queue of ErrorRecords injected from parents transitive. + */ + get errorsQueue(): ErrorRecord[] { + return this._errorsQueue; + } + + /** + * ** Flag to instruct component to show all processed error or to peak the most important one, which is latest, according the HTTP Status code. + */ + @Input() renderAllErrors = false; + + /** + * ** Array of error codes for which Placeholder should listen and on ChangeDetection cycle to look for exact match into errorsQueue. + */ + @Input() listenForErrors: string[] = []; + + /** + * ** Array of error codes pattern for which Placeholder should listen and on ChangeDetection cycle to look for match into errorsQueue. + */ + @Input() listenForErrorPatterns: string[] = []; + + /** + * ** Error context. + * + * - Visualized only if listened error code(s) exist in errorsQueue. + */ + get errorContext(): string { + return this._errorContext; + } + + /** + * ** Error context. + */ + @Input() set errorContext(value: string) { + if (CollectionsUtil.isString(value) && value.length > 0) { + this._errorContext = value; + } else { + this._errorContext = "Page data"; } - - /** - * ** Empty state image opacity. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - @Input() set emptyImgOpacity(value: number) { - if (CollectionsUtil.isNumber(value) && value >= 1 && value <= 1) { - this._emptyImgOpacity = value; - } else { - this._emptyImgOpacity = EmptyImgStyle.Opacity; - } + } + + /** + * ** Context singular or plural. + * + * - If true - it's plural + * - If false - it's singular + */ + @Input() plural = false; + + /** + * ** Flag that identifies if error ot empty state template should be rendered. + */ + showError = false; + + /** + * ** Flag that indicates for error template is used system default. + */ + isErrorTemplateSystemDefault = false; + + /** + * ** Identified ErrorRecords in {@link errorsQueue} that exact match {@link listenForErrors} or match {@link listenForErrorPatterns} + * + * - injected to custom error template if provided + * - used when only one error should be shown + */ + identifiedErrors: ErrorRecord[] = []; + + /** + * ** Identified ErrorRecord in {@link errorsQueue} that exact match {@link listenForErrors} or match {@link listenForErrorPatterns} + * + * - injected to custom error template if provided + * - used when only one error should be shown + */ + identifiedErrorWithApiMessage: IdentifiedErrorRecordWithMessage = { + record: [], + problem: "", + description: "", + mitigation: "", + escalation: "", + impactedServices: "", + apiMessage: null, + imageSrc: "", + imageWidth: ErrorImgStyle.Width.Generic, + imageOpacity: ErrorImgStyle.Opacity, + }; + + /** + * ** Identified ErrorRecords in {@link errorsQueue} that exact match {@link listenForErrors} or match {@link listenForErrorPatterns} + * + * - injected to custom error template if provided + * - used if iteration of errors is requested + */ + identifiedErrorsWithApiMessage: IdentifiedErrorRecordWithMessage[] = []; + + // empty state private fields + private _emptyMessage = EmptyMessage.Generic; + private _isEmptyMessageExternal = false; + + private _emptyImgSrc: string = EmptyImgSource.Generic; + private _isEmptyImgSrcExternal = false; + + private _emptyImgWidth = EmptyImgStyle.Width; + private _emptyImgOpacity = EmptyImgStyle.Opacity; + + private _hideDefaultEmptyStateImageInGrid = false; + + // error state private fields + private _errorsQueue: ErrorRecord[] = []; + + private _errorContext = "Page data"; + + /** + * ** Constructor. + */ + constructor( + private readonly elementRef: ElementRef, + private readonly renderer2: Renderer2, + private readonly placeholderService: PlaceholderService, + @Inject(SHARED_FEATURES_CONFIG_TOKEN) + private readonly featureConfig: PlaceholderConfig, + ) { + super(); + + const serviceRequestUrl = featureConfig?.placeholder?.serviceRequestUrl + ? featureConfig.placeholder.serviceRequestUrl + : "#"; + + ErrorEscalation.Generic = CollectionsUtil.interpolateString( + ErrorEscalation.Generic, + { + searchValue: "%service_req_url%", + replaceValue: serviceRequestUrl, + }, + ); + + ErrorEscalation.NotFound = CollectionsUtil.interpolateString( + ErrorEscalation.NotFound, + { + searchValue: "%service_req_url%", + replaceValue: serviceRequestUrl, + }, + ); + } + + /** + * ** Resolves custom or system error for identified Error. + */ + resolveErrorTemplate( + identifiedErrorRecordWithMessage: IdentifiedErrorRecordWithMessage, + ): TemplateRef { + const httpStatusCode = + identifiedErrorRecordWithMessage?.record[0]?.httpStatusCode; + + let templateRef: TemplateRef; + + // find exact match for custom template depend on Http Status Code + switch (httpStatusCode) { + case HttpStatusCode.BadRequest: + templateRef = this.errorTemplateRefBadRequest; + break; + case HttpStatusCode.Unauthorized: + templateRef = this.errorTemplateRefUnauthorized; + break; + case HttpStatusCode.Forbidden: + templateRef = this.errorTemplateRefForbidden; + break; + case HttpStatusCode.NotFound: + templateRef = this.errorTemplateRefNotFound; + break; + case HttpStatusCode.MethodNotAllowed: + templateRef = this.errorTemplateRefMethodNotAllowed; + break; + case HttpStatusCode.Conflict: + templateRef = this.errorTemplateRefConflict; + break; + case HttpStatusCode.UnprocessableEntity: + templateRef = this.errorTemplateRefUnprocessableEntity; + break; + case HttpStatusCode.InternalServerError: + templateRef = this.errorTemplateRefInternalServerError; + break; + case HttpStatusCode.ServiceUnavailable: + templateRef = this.errorTemplateRefServiceUnavailable; + break; + default: + // No-op. } - /** - * ** Empty state image opacity. - * - * - Visualized only if component is rendered without errors or there is no listened error code(s). - */ - get emptyImgOpacity(): number { - return this._emptyImgOpacity; - } + this.isErrorTemplateSystemDefault = false; - /** - * ** Flag to show or hide custom empty state image. - * - * - default value is FALSE. - */ - @Input() showCustomEmptyStateImage = false; - - /** - * ** Flag to show or hide default empty state image in grid. - * - * - default value is FALSE. - */ - @Input() hideDefaultEmptyStateImageInGrid = false; - - // error state - - /** - * ** Errors queue of ErrorRecords injected from parents transitive. - */ - @Input() set errorsQueue(value: ErrorRecord[]) { - if (CollectionsUtil.isArray(value)) { - this._errorsQueue = value; - } else { - this._errorsQueue = []; - } + // if found exact match for custom template to Http Status Code return immediately + if (templateRef instanceof TemplateRef) { + return templateRef; } - /** - * ** Errors queue of ErrorRecords injected from parents transitive. - */ - get errorsQueue(): ErrorRecord[] { - return this._errorsQueue; + // find match for custom template depend on group 4xx of Http Status Codes + if ( + httpStatusCode >= 400 && + httpStatusCode < 500 && + this.errorTemplateRefClientErrors instanceof TemplateRef + ) { + return this.errorTemplateRefClientErrors; } - /** - * ** Flag to instruct component to show all processed error or to peak the most important one, which is latest, according the HTTP Status code. - */ - @Input() renderAllErrors = false; - - /** - * ** Array of error codes for which Placeholder should listen and on ChangeDetection cycle to look for exact match into errorsQueue. - */ - @Input() listenForErrors: string[] = []; - - /** - * ** Array of error codes pattern for which Placeholder should listen and on ChangeDetection cycle to look for match into errorsQueue. - */ - @Input() listenForErrorPatterns: string[] = []; - - /** - * ** Error context. - * - * - Visualized only if listened error code(s) exist in errorsQueue. - */ - get errorContext(): string { - return this._errorContext; + // find match for custom template depend on group 5xx of Http Status Codes + if ( + httpStatusCode >= 500 && + this.errorTemplateRefServerErrors instanceof TemplateRef + ) { + return this.errorTemplateRefServerErrors; } - /** - * ** Error context. - */ - @Input() set errorContext(value: string) { - if (CollectionsUtil.isString(value) && value.length > 0) { - this._errorContext = value; - } else { - this._errorContext = 'Page data'; - } + // fallback to custom generic error template + if (this.errorTemplateRefGeneric instanceof TemplateRef) { + return this.errorTemplateRefGeneric; } - /** - * ** Context singular or plural. - * - * - If true - it's plural - * - If false - it's singular - */ - @Input() plural = false; - - /** - * ** Flag that identifies if error ot empty state template should be rendered. - */ - showError = false; - - /** - * ** Flag that indicates for error template is used system default. - */ - isErrorTemplateSystemDefault = false; - - /** - * ** Identified ErrorRecords in {@link errorsQueue} that exact match {@link listenForErrors} or match {@link listenForErrorPatterns} - * - * - injected to custom error template if provided - * - used when only one error should be shown - */ - identifiedErrors: ErrorRecord[] = []; - - /** - * ** Identified ErrorRecord in {@link errorsQueue} that exact match {@link listenForErrors} or match {@link listenForErrorPatterns} - * - * - injected to custom error template if provided - * - used when only one error should be shown - */ - identifiedErrorWithApiMessage: IdentifiedErrorRecordWithMessage = { - record: [], - problem: '', - description: '', - mitigation: '', - escalation: '', - impactedServices: '', - apiMessage: null, - imageSrc: '', - imageWidth: ErrorImgStyle.Width.Generic, - imageOpacity: ErrorImgStyle.Opacity - }; - - /** - * ** Identified ErrorRecords in {@link errorsQueue} that exact match {@link listenForErrors} or match {@link listenForErrorPatterns} - * - * - injected to custom error template if provided - * - used if iteration of errors is requested - */ - identifiedErrorsWithApiMessage: IdentifiedErrorRecordWithMessage[] = []; - - // empty state private fields - private _emptyMessage = EmptyMessage.Generic; - private _isEmptyMessageExternal = false; - - private _emptyImgSrc: string = EmptyImgSource.Generic; - private _isEmptyImgSrcExternal = false; - - private _emptyImgWidth = EmptyImgStyle.Width; - private _emptyImgOpacity = EmptyImgStyle.Opacity; - - private _hideDefaultEmptyStateImageInGrid = false; - - // error state private fields - private _errorsQueue: ErrorRecord[] = []; - - private _errorContext = 'Page data'; - - /** - * ** Constructor. - */ - constructor( - private readonly elementRef: ElementRef, - private readonly renderer2: Renderer2, - private readonly placeholderService: PlaceholderService, - @Inject(SHARED_FEATURES_CONFIG_TOKEN) private readonly featureConfig: PlaceholderConfig + this.isErrorTemplateSystemDefault = true; + + // return system default error template + return this.errorTemplateRefSystemDefault; + } + + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + if ( + PlaceholderComponent._isPropertyChanged(changes, "loading") || + PlaceholderComponent._isPropertyChanged(changes, "errorsQueue") || + PlaceholderComponent._isPropertyChanged(changes, "listenForErrors") || + PlaceholderComponent._isPropertyChanged(changes, "listenForErrorPatterns") ) { - super(); - - const serviceRequestUrl = featureConfig?.placeholder?.serviceRequestUrl ? featureConfig.placeholder.serviceRequestUrl : '#'; - - ErrorEscalation.Generic = CollectionsUtil.interpolateString(ErrorEscalation.Generic, { - searchValue: '%service_req_url%', - replaceValue: serviceRequestUrl - }); - - ErrorEscalation.NotFound = CollectionsUtil.interpolateString(ErrorEscalation.NotFound, { - searchValue: '%service_req_url%', - replaceValue: serviceRequestUrl - }); + this._executeRefineCycle(); } - - /** - * ** Resolves custom or system error for identified Error. - */ - resolveErrorTemplate(identifiedErrorRecordWithMessage: IdentifiedErrorRecordWithMessage): TemplateRef { - const httpStatusCode = identifiedErrorRecordWithMessage?.record[0]?.httpStatusCode; - - let templateRef: TemplateRef; - - // find exact match for custom template depend on Http Status Code - switch (httpStatusCode) { - case HttpStatusCode.BadRequest: - templateRef = this.errorTemplateRefBadRequest; - break; - case HttpStatusCode.Unauthorized: - templateRef = this.errorTemplateRefUnauthorized; - break; - case HttpStatusCode.Forbidden: - templateRef = this.errorTemplateRefForbidden; - break; - case HttpStatusCode.NotFound: - templateRef = this.errorTemplateRefNotFound; - break; - case HttpStatusCode.MethodNotAllowed: - templateRef = this.errorTemplateRefMethodNotAllowed; - break; - case HttpStatusCode.Conflict: - templateRef = this.errorTemplateRefConflict; - break; - case HttpStatusCode.UnprocessableEntity: - templateRef = this.errorTemplateRefUnprocessableEntity; - break; - case HttpStatusCode.InternalServerError: - templateRef = this.errorTemplateRefInternalServerError; - break; - case HttpStatusCode.ServiceUnavailable: - templateRef = this.errorTemplateRefServiceUnavailable; - break; - default: - // No-op. - } - - this.isErrorTemplateSystemDefault = false; - - // if found exact match for custom template to Http Status Code return immediately - if (templateRef instanceof TemplateRef) { - return templateRef; - } - - // find match for custom template depend on group 4xx of Http Status Codes - if (httpStatusCode >= 400 && httpStatusCode < 500 && this.errorTemplateRefClientErrors instanceof TemplateRef) { - return this.errorTemplateRefClientErrors; - } - - // find match for custom template depend on group 5xx of Http Status Codes - if (httpStatusCode >= 500 && this.errorTemplateRefServerErrors instanceof TemplateRef) { - return this.errorTemplateRefServerErrors; - } - - // fallback to custom generic error template - if (this.errorTemplateRefGeneric instanceof TemplateRef) { - return this.errorTemplateRefGeneric; - } - - this.isErrorTemplateSystemDefault = true; - - // return system default error template - return this.errorTemplateRefSystemDefault; + } + + /** + * @inheritDoc + */ + ngOnInit(): void { + this._executeRefineCycle(); + } + + private _executeRefineCycle(): void { + try { + this._refineErrorState(); + this.placeholderService.refineElementsState( + this.elementRef, + this._hideDefaultEmptyStateImageInGrid, + ); + } catch (e) { + console.error(e); } - - /** - * @inheritDoc - */ - ngOnChanges(changes: SimpleChanges): void { - if ( - PlaceholderComponent._isPropertyChanged(changes, 'loading') || - PlaceholderComponent._isPropertyChanged(changes, 'errorsQueue') || - PlaceholderComponent._isPropertyChanged(changes, 'listenForErrors') || - PlaceholderComponent._isPropertyChanged(changes, 'listenForErrorPatterns') - ) { - this._executeRefineCycle(); - } + } + + private _refineErrorState(): void { + // filter error records and return only records that for component is listening + const filteredErrorRecords = filterErrorRecords( + this.errorsQueue, + this.listenForErrors, + this.listenForErrorPatterns, + ); + + // listened errors records exist, then show error state + this.showError = filteredErrorRecords.length > 0; + + // check is empty state image in grid should be hidden + // if error always hide + // if empty hide/show depends on injected flag + this._hideDefaultEmptyStateImageInGrid = + this.showError || this.hideDefaultEmptyStateImageInGrid; + + // reset buffers that transport data to view template (HTML template) + this._resetTransportBuffers(); + + if (!this.showError) { + return; } - /** - * @inheritDoc - */ - ngOnInit(): void { - this._executeRefineCycle(); + // populate buffers that transport data to view template (HTML template) + this._populateTransportBuffers(filteredErrorRecords); + } + + private _resetTransportBuffers(): void { + this.identifiedErrors = []; + + this.identifiedErrorWithApiMessage = { + record: [], + problem: "", + description: "", + mitigation: "", + escalation: "", + impactedServices: "", + apiMessage: null, + imageSrc: "", + imageWidth: ErrorImgStyle.Width.Generic, + imageOpacity: ErrorImgStyle.Opacity, + }; + this.identifiedErrorsWithApiMessage = []; + } + + private _populateTransportBuffers(errorRecords: ErrorRecord[]): void { + this.identifiedErrors = [...errorRecords]; + this.identifiedErrorWithApiMessage = + this._getErrorsToIdentifiedError(errorRecords); + this.identifiedErrorsWithApiMessage = errorRecords.map((record) => + this._getErrorToIdentifiedError(record), + ); + } + + private _getErrorToIdentifiedError( + record: ErrorRecord, + ): IdentifiedErrorRecordWithMessage { + if (window && window.navigator && !window.navigator.onLine) { + return { + record: [record], + problem: ErrorProblem.Offline, + description: ErrorDescription.Offline, + mitigation: ErrorMitigation.Offline, + escalation: ErrorEscalation.Offline, + impactedServices: "", + apiMessage: this.placeholderService.extractErrorInformation( + record.error, + ), + imageSrc: ErrorImgSource.Offline, + imageWidth: ErrorImgStyle.Width.Offline, + imageOpacity: ErrorImgStyle.Opacity, + }; } - private _executeRefineCycle(): void { - try { - this._refineErrorState(); - this.placeholderService.refineElementsState(this.elementRef, this._hideDefaultEmptyStateImageInGrid); - } catch (e) { - console.error(e); - } + if (record && record.httpStatusCode === HttpStatusCode.NotFound) { + return { + record: [record], + problem: CollectionsUtil.interpolateString( + ErrorProblem.NotFound, + this._errorContext, + this.plural ? "are" : "is", + ), + description: CollectionsUtil.interpolateString( + ErrorDescription.NotFound, + this._errorContext, + this.plural ? "are" : "is", + ), + mitigation: ErrorMitigation.NotFound, + escalation: ErrorEscalation.NotFound, + impactedServices: "", + apiMessage: this.placeholderService.extractErrorInformation( + record.error, + ), + imageSrc: ErrorImgSource.NotFound, + imageWidth: ErrorImgStyle.Width.NotFound, + imageOpacity: ErrorImgStyle.Opacity, + }; } - private _refineErrorState(): void { - // filter error records and return only records that for component is listening - const filteredErrorRecords = filterErrorRecords(this.errorsQueue, this.listenForErrors, this.listenForErrorPatterns); - - // listened errors records exist, then show error state - this.showError = filteredErrorRecords.length > 0; - - // check is empty state image in grid should be hidden - // if error always hide - // if empty hide/show depends on injected flag - this._hideDefaultEmptyStateImageInGrid = this.showError || this.hideDefaultEmptyStateImageInGrid; - - // reset buffers that transport data to view template (HTML template) - this._resetTransportBuffers(); - - if (!this.showError) { - return; - } - - // populate buffers that transport data to view template (HTML template) - this._populateTransportBuffers(filteredErrorRecords); + return { + record: [record], + problem: CollectionsUtil.interpolateString( + ErrorProblem.Generic, + this._errorContext, + this.plural ? "are" : "is", + ), + description: CollectionsUtil.interpolateString( + ErrorDescription.Generic, + this._errorContext, + this.plural ? "are" : "is", + ), + mitigation: ErrorMitigation.Generic, + escalation: ErrorEscalation.Generic, + impactedServices: PlaceholderService.extractClassPublicName(record), + apiMessage: this.placeholderService.extractErrorInformation(record.error), + imageSrc: ErrorImgSource.Generic, + imageWidth: ErrorImgStyle.Width.Generic, + imageOpacity: ErrorImgStyle.Opacity, + }; + } + + private _getErrorsToIdentifiedError( + errorRecords: ErrorRecord[], + ): IdentifiedErrorRecordWithMessage { + if (window && window.navigator && !window.navigator.onLine) { + return { + record: errorRecords, + problem: ErrorProblem.Offline, + description: ErrorDescription.Offline, + mitigation: ErrorMitigation.Offline, + escalation: ErrorEscalation.Offline, + impactedServices: "", + apiMessage: this.placeholderService.extractErrorInformation( + new Error("No Internet Connection"), + ), + imageSrc: ErrorImgSource.Offline, + imageWidth: ErrorImgStyle.Width.Offline, + imageOpacity: ErrorImgStyle.Opacity, + }; } - private _resetTransportBuffers(): void { - this.identifiedErrors = []; - - this.identifiedErrorWithApiMessage = { - record: [], - problem: '', - description: '', - mitigation: '', - escalation: '', - impactedServices: '', - apiMessage: null, - imageSrc: '', - imageWidth: ErrorImgStyle.Width.Generic, - imageOpacity: ErrorImgStyle.Opacity - }; - this.identifiedErrorsWithApiMessage = []; + const latestIdentifiedError: ErrorRecord = errorRecords[0]; + let filteredErrors: ErrorRecord[] = []; + let filteredErrorsTmp: ErrorRecord[]; + + // search for ServiceUnavailable 503 + filteredErrorsTmp = errorRecords.filter( + (r) => r.httpStatusCode === HttpStatusCode.ServiceUnavailable, + ); + if (filteredErrorsTmp.length > 0) { + // if time between latest and identified service unavailable is less than 2s show Service Unavailable + if ( + latestIdentifiedError.code === filteredErrorsTmp[0].code || + latestIdentifiedError.time - filteredErrorsTmp[0].time < 2000 + ) { + filteredErrors = filteredErrorsTmp; + } } - private _populateTransportBuffers(errorRecords: ErrorRecord[]): void { - this.identifiedErrors = [...errorRecords]; - this.identifiedErrorWithApiMessage = this._getErrorsToIdentifiedError(errorRecords); - this.identifiedErrorsWithApiMessage = errorRecords.map((record) => this._getErrorToIdentifiedError(record)); + // search for InternalServerError 500 + filteredErrorsTmp = errorRecords.filter( + (r) => r.httpStatusCode === HttpStatusCode.InternalServerError, + ); + if (filteredErrors.length === 0 && filteredErrorsTmp.length > 0) { + // if time between latest and identified internal server error is less than 2s show Internal Server Error + if ( + latestIdentifiedError.code === filteredErrorsTmp[0].code || + latestIdentifiedError.time - filteredErrorsTmp[0].time < 2000 + ) { + filteredErrors = filteredErrorsTmp; + } } - private _getErrorToIdentifiedError(record: ErrorRecord): IdentifiedErrorRecordWithMessage { - if (window && window.navigator && !window.navigator.onLine) { - return { - record: [record], - problem: ErrorProblem.Offline, - description: ErrorDescription.Offline, - mitigation: ErrorMitigation.Offline, - escalation: ErrorEscalation.Offline, - impactedServices: '', - apiMessage: this.placeholderService.extractErrorInformation(record.error), - imageSrc: ErrorImgSource.Offline, - imageWidth: ErrorImgStyle.Width.Offline, - imageOpacity: ErrorImgStyle.Opacity - }; - } - - if (record && record.httpStatusCode === HttpStatusCode.NotFound) { - return { - record: [record], - problem: CollectionsUtil.interpolateString(ErrorProblem.NotFound, this._errorContext, this.plural ? 'are' : 'is'), - description: CollectionsUtil.interpolateString(ErrorDescription.NotFound, this._errorContext, this.plural ? 'are' : 'is'), - mitigation: ErrorMitigation.NotFound, - escalation: ErrorEscalation.NotFound, - impactedServices: '', - apiMessage: this.placeholderService.extractErrorInformation(record.error), - imageSrc: ErrorImgSource.NotFound, - imageWidth: ErrorImgStyle.Width.NotFound, - imageOpacity: ErrorImgStyle.Opacity - }; - } + // search for NotFound 404 + filteredErrorsTmp = errorRecords.filter( + (r) => r.httpStatusCode === HttpStatusCode.NotFound, + ); + if (filteredErrors.length === 0 && filteredErrorsTmp.length > 0) { + // if time between latest and identified not found is less than 2s show Not Found + if ( + latestIdentifiedError.code === filteredErrorsTmp[0].code || + latestIdentifiedError.time - filteredErrorsTmp[0].time < 2000 + ) { + filteredErrors = filteredErrorsTmp; return { - record: [record], - problem: CollectionsUtil.interpolateString(ErrorProblem.Generic, this._errorContext, this.plural ? 'are' : 'is'), - description: CollectionsUtil.interpolateString(ErrorDescription.Generic, this._errorContext, this.plural ? 'are' : 'is'), - mitigation: ErrorMitigation.Generic, - escalation: ErrorEscalation.Generic, - impactedServices: PlaceholderService.extractClassPublicName(record), - apiMessage: this.placeholderService.extractErrorInformation(record.error), - imageSrc: ErrorImgSource.Generic, - imageWidth: ErrorImgStyle.Width.Generic, - imageOpacity: ErrorImgStyle.Opacity + record: filteredErrors, + problem: CollectionsUtil.interpolateString( + ErrorProblem.NotFound, + this._errorContext, + this.plural ? "are" : "is", + ), + description: CollectionsUtil.interpolateString( + ErrorDescription.NotFound, + this._errorContext, + this.plural ? "are" : "is", + ), + mitigation: ErrorMitigation.NotFound, + escalation: ErrorEscalation.NotFound, + impactedServices: "", + apiMessage: this.placeholderService.extractErrorInformation( + filteredErrors[0].error, + ), + imageSrc: ErrorImgSource.NotFound, + imageWidth: ErrorImgStyle.Width.NotFound, + imageOpacity: ErrorImgStyle.Opacity, }; + } } - private _getErrorsToIdentifiedError(errorRecords: ErrorRecord[]): IdentifiedErrorRecordWithMessage { - if (window && window.navigator && !window.navigator.onLine) { - return { - record: errorRecords, - problem: ErrorProblem.Offline, - description: ErrorDescription.Offline, - mitigation: ErrorMitigation.Offline, - escalation: ErrorEscalation.Offline, - impactedServices: '', - apiMessage: this.placeholderService.extractErrorInformation(new Error('No Internet Connection')), - imageSrc: ErrorImgSource.Offline, - imageWidth: ErrorImgStyle.Width.Offline, - imageOpacity: ErrorImgStyle.Opacity - }; - } - - const latestIdentifiedError: ErrorRecord = errorRecords[0]; - let filteredErrors: ErrorRecord[] = []; - let filteredErrorsTmp: ErrorRecord[]; - - // search for ServiceUnavailable 503 - filteredErrorsTmp = errorRecords.filter((r) => r.httpStatusCode === HttpStatusCode.ServiceUnavailable); - if (filteredErrorsTmp.length > 0) { - // if time between latest and identified service unavailable is less than 2s show Service Unavailable - if (latestIdentifiedError.code === filteredErrorsTmp[0].code || latestIdentifiedError.time - filteredErrorsTmp[0].time < 2000) { - filteredErrors = filteredErrorsTmp; - } - } - - // search for InternalServerError 500 - filteredErrorsTmp = errorRecords.filter((r) => r.httpStatusCode === HttpStatusCode.InternalServerError); - if (filteredErrors.length === 0 && filteredErrorsTmp.length > 0) { - // if time between latest and identified internal server error is less than 2s show Internal Server Error - if (latestIdentifiedError.code === filteredErrorsTmp[0].code || latestIdentifiedError.time - filteredErrorsTmp[0].time < 2000) { - filteredErrors = filteredErrorsTmp; - } - } - - // search for NotFound 404 - filteredErrorsTmp = errorRecords.filter((r) => r.httpStatusCode === HttpStatusCode.NotFound); - if (filteredErrors.length === 0 && filteredErrorsTmp.length > 0) { - // if time between latest and identified not found is less than 2s show Not Found - if (latestIdentifiedError.code === filteredErrorsTmp[0].code || latestIdentifiedError.time - filteredErrorsTmp[0].time < 2000) { - filteredErrors = filteredErrorsTmp; - - return { - record: filteredErrors, - problem: CollectionsUtil.interpolateString(ErrorProblem.NotFound, this._errorContext, this.plural ? 'are' : 'is'), - description: CollectionsUtil.interpolateString( - ErrorDescription.NotFound, - this._errorContext, - this.plural ? 'are' : 'is' - ), - mitigation: ErrorMitigation.NotFound, - escalation: ErrorEscalation.NotFound, - impactedServices: '', - apiMessage: this.placeholderService.extractErrorInformation(filteredErrors[0].error), - imageSrc: ErrorImgSource.NotFound, - imageWidth: ErrorImgStyle.Width.NotFound, - imageOpacity: ErrorImgStyle.Opacity - }; - } - } - - // fallback to all identified services - if (filteredErrors.length === 0) { - filteredErrors = errorRecords; - } - - return { - record: filteredErrors, - problem: CollectionsUtil.interpolateString(ErrorProblem.Generic, this._errorContext, this.plural ? 'are' : 'is'), - description: CollectionsUtil.interpolateString(ErrorDescription.Generic, this._errorContext), - mitigation: ErrorMitigation.Generic, - escalation: ErrorEscalation.Generic, - impactedServices: PlaceholderService.extractClassesPublicNames(filteredErrors), - apiMessage: this.placeholderService.extractErrorInformation(filteredErrors[0].error), - imageSrc: ErrorImgSource.Generic, - imageWidth: ErrorImgStyle.Width.Generic, - imageOpacity: ErrorImgStyle.Opacity - }; + // fallback to all identified services + if (filteredErrors.length === 0) { + filteredErrors = errorRecords; } - private static _isPropertyChanged(changes: SimpleChanges, field: string): boolean { - return changes[field] && changes[field].currentValue !== changes[field].previousValue; - } + return { + record: filteredErrors, + problem: CollectionsUtil.interpolateString( + ErrorProblem.Generic, + this._errorContext, + this.plural ? "are" : "is", + ), + description: CollectionsUtil.interpolateString( + ErrorDescription.Generic, + this._errorContext, + ), + mitigation: ErrorMitigation.Generic, + escalation: ErrorEscalation.Generic, + impactedServices: + PlaceholderService.extractClassesPublicNames(filteredErrors), + apiMessage: this.placeholderService.extractErrorInformation( + filteredErrors[0].error, + ), + imageSrc: ErrorImgSource.Generic, + imageWidth: ErrorImgStyle.Width.Generic, + imageOpacity: ErrorImgStyle.Opacity, + }; + } + + private static _isPropertyChanged( + changes: SimpleChanges, + field: string, + ): boolean { + return ( + changes[field] && + changes[field].currentValue !== changes[field].previousValue + ); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/index.ts index c0062a3443..ee8c7eb032 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './model'; -export * from './components'; +export * from "./model"; +export * from "./components"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/index.ts index 9941e6a9a2..4878d5c362 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './placeholder.model'; +export * from "./placeholder.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/placeholder.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/placeholder.model.ts index b537816087..e05820c566 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/placeholder.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/model/placeholder.model.ts @@ -9,15 +9,15 @@ * - Should be provided when Module is initialized and only once. */ export interface PlaceholderConfig { - [PLACEHOLDER_FEATURE_KEY]: { - /** - * ** Url that will open portal, where service request should be created. - */ - serviceRequestUrl: string; - }; + [PLACEHOLDER_FEATURE_KEY]: { + /** + * ** Url that will open portal, where service request should be created. + */ + serviceRequestUrl: string; + }; } /** * ** Key for the feature in Shared Features config object. */ -const PLACEHOLDER_FEATURE_KEY = 'placeholder'; +const PLACEHOLDER_FEATURE_KEY = "placeholder"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/placeholder.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/placeholder.module.ts index 932610397d..46b430ab0d 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/placeholder.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/placeholder.module.ts @@ -3,20 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; +import { CommonModule } from "@angular/common"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { EmptyStateComponent, PlaceholderComponent } from './components'; +import { EmptyStateComponent, PlaceholderComponent } from "./components"; /** * ** Placeholder module */ @NgModule({ - imports: [CommonModule, ClarityModule], - declarations: [EmptyStateComponent, PlaceholderComponent], - exports: [EmptyStateComponent, PlaceholderComponent], - schemas: [NO_ERRORS_SCHEMA] + imports: [CommonModule, ClarityModule], + declarations: [EmptyStateComponent, PlaceholderComponent], + exports: [EmptyStateComponent, PlaceholderComponent], + schemas: [NO_ERRORS_SCHEMA], }) export class PlaceholderModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/public-api.ts index ff9e5e20e4..db407019e0 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/public-api.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { EmptyStateComponent, PlaceholderComponent } from './components'; -export { PlaceholderModule } from './placeholder.module'; +export { EmptyStateComponent, PlaceholderComponent } from "./components"; +export { PlaceholderModule } from "./placeholder.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/index.ts index 6076e82bb4..14cbb3dfa0 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './placeholder.service'; +export * from "./placeholder.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.spec.ts index 9265c204c3..5d04e1a92c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.spec.ts @@ -8,235 +8,296 @@ @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access */ -import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement, ElementRef, HostListener, OnInit, Renderer2 } from '@angular/core'; -import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; - -import { CollectionsUtil } from '../../../utils'; - -import { ApiErrorMessage, ErrorRecord, generateErrorCode } from '../../../common'; - -import { PlaceholderService } from './placeholder.service'; +import { + Component, + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, + ElementRef, + HostListener, + OnInit, + Renderer2, +} from "@angular/core"; +import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http"; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, +} from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { CollectionsUtil } from "../../../utils"; + +import { + ApiErrorMessage, + ErrorRecord, + generateErrorCode, +} from "../../../common"; + +import { PlaceholderService } from "./placeholder.service"; @Component({ - selector: 'shared-placeholder', - template: ` -
    -

    Some placeholder

    -
    - `, - providers: [PlaceholderService] + selector: "shared-placeholder", + template: ` +
    +

    Some placeholder

    +
    + `, + providers: [PlaceholderService], }) class PlaceholderComponentStub implements OnInit { - @HostListener('changeHideDefaultStateImageInGrid', ['$event']) changeHideImageInGrid($event: { value: boolean }) { - this._hideDefaultEmptyStateImageInGrid = $event.value; - - this._executeRefineCycle(); - } - - private _hideDefaultEmptyStateImageInGrid = false; - - constructor( - public readonly elementRef: ElementRef, - public readonly renderer2: Renderer2, - private readonly placeholderService: PlaceholderService - ) {} - - ngOnInit(): void { - this._executeRefineCycle(); - } - - private _executeRefineCycle(): void { - this.placeholderService.refineElementsState(this.elementRef, this._hideDefaultEmptyStateImageInGrid); - } + @HostListener("changeHideDefaultStateImageInGrid", ["$event"]) + changeHideImageInGrid($event: { value: boolean }) { + this._hideDefaultEmptyStateImageInGrid = $event.value; + + this._executeRefineCycle(); + } + + private _hideDefaultEmptyStateImageInGrid = false; + + constructor( + public readonly elementRef: ElementRef, + public readonly renderer2: Renderer2, + private readonly placeholderService: PlaceholderService, + ) {} + + ngOnInit(): void { + this._executeRefineCycle(); + } + + private _executeRefineCycle(): void { + this.placeholderService.refineElementsState( + this.elementRef, + this._hideDefaultEmptyStateImageInGrid, + ); + } } @Component({ - selector: 'clr-datagrid', - template: ` -
    -
    -
    - -
    -
    + selector: "clr-datagrid", + template: ` +
    +
    +
    +
    - ` +
    +
    + `, }) class ClarityDataGridComponentStub { - constructor(public readonly elementRef: ElementRef) {} + constructor(public readonly elementRef: ElementRef) {} } @Component({ - selector: 'shared-container', - template: ` -
    - -
    - ` + selector: "shared-container", + template: ` +
    + +
    + `, }) class StandaloneComponentStub { - constructor(public readonly elementRef: ElementRef) {} + constructor(public readonly elementRef: ElementRef) {} } @Component({ - selector: 'test-component', - template: ` -
    - -
    - ` + selector: "test-component", + template: ` +
    + +
    + `, }) class TestComponentStub {} -describe('PlaceholderService', () => { - let randomStringGrid: string; - let randomStringStandalone: string; - - let fixture: ComponentFixture; - - let gridDebugElement: DebugElement; - let gridComponent: ClarityDataGridComponentStub; - - let placeholderDebugElement: DebugElement; - let placeholderComponent: PlaceholderComponentStub; - - let renderer2: Renderer2; +describe("PlaceholderService", () => { + let randomStringGrid: string; + let randomStringStandalone: string; - let service: PlaceholderService; + let fixture: ComponentFixture; - beforeEach(() => { - randomStringGrid = 'fsklmxksfdksaj'; - randomStringStandalone = 'dsaasdfsdffff'; - spyOn(CollectionsUtil, 'generateRandomString').and.returnValues( - randomStringGrid, - randomStringStandalone, - randomStringGrid, - randomStringStandalone - ); + let gridDebugElement: DebugElement; + let gridComponent: ClarityDataGridComponentStub; - TestBed.configureTestingModule({ - declarations: [PlaceholderComponentStub, ClarityDataGridComponentStub, TestComponentStub], - providers: [Renderer2], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); + let placeholderDebugElement: DebugElement; + let placeholderComponent: PlaceholderComponentStub; - fixture = TestBed.createComponent(TestComponentStub); + let renderer2: Renderer2; - gridDebugElement = fixture.debugElement.query(By.directive(ClarityDataGridComponentStub)); - gridComponent = gridDebugElement.componentInstance; + let service: PlaceholderService; - placeholderDebugElement = fixture.debugElement.query(By.directive(PlaceholderComponentStub)); - placeholderComponent = placeholderDebugElement.componentInstance; - - renderer2 = placeholderComponent.renderer2; - - service = placeholderDebugElement.injector.get(PlaceholderService); - }); + beforeEach(() => { + randomStringGrid = "fsklmxksfdksaj"; + randomStringStandalone = "dsaasdfsdffff"; + spyOn(CollectionsUtil, "generateRandomString").and.returnValues( + randomStringGrid, + randomStringStandalone, + randomStringGrid, + randomStringStandalone, + ); - it('should be created', () => { - expect(service).toBeTruthy(); + TestBed.configureTestingModule({ + declarations: [ + PlaceholderComponentStub, + ClarityDataGridComponentStub, + TestComponentStub, + ], + providers: [Renderer2], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }); - describe('Statics::', () => { - describe('Methods::', () => { - let errorCodes: string[]; - let errorRecords: ErrorRecord[]; - - beforeEach(() => { - errorCodes = [ - generateErrorCode(PlaceholderService.CLASS_NAME, PlaceholderService.PUBLIC_NAME, 'refineElementsState', '500'), - generateErrorCode(PlaceholderService.CLASS_NAME, PlaceholderService.PUBLIC_NAME, 'refineElementsState', '503'), - generateErrorCode('DataLoadApiService', 'Data-Load-Api-Service', 'loadData', '422') - ]; - errorRecords = [ - { - code: errorCodes[0], - error: new HttpErrorResponse({ - error: new Error('Some Error 1'), - status: 500 - }), - httpStatusCode: 500, - objectUUID: 'SomeObjectUUID_1' - }, - { - code: errorCodes[1], - error: new HttpErrorResponse({ - error: new Error('Some Error 2'), - status: 503 - }), - httpStatusCode: 503, - objectUUID: 'SomeObjectUUID_1' - }, - { - code: errorCodes[2], - error: new HttpErrorResponse({ - error: new Error('Some Error 3'), - status: 422 - }), - httpStatusCode: 422, - objectUUID: 'SomeObjectUUID_2' - } - ]; - }); - - describe('|extractClassPublicName|', () => { - it('should verify will extract Class Public Name', () => { - // When - const publicName1 = PlaceholderService.extractClassPublicName(errorRecords[0]); - const publicName2 = PlaceholderService.extractClassPublicName(errorRecords[1]); - const publicName3 = PlaceholderService.extractClassPublicName(errorRecords[2]); - const publicName4 = PlaceholderService.extractClassPublicName(null); - - // Then - expect(publicName1).toEqual('Placeholder service'); - expect(publicName2).toEqual('Placeholder service'); - expect(publicName3).toEqual('Data load api service'); - expect(publicName4).toEqual(''); - }); - }); - - describe('|extractClassesPublicNames|', () => { - it('should verify will extract Class Public Names of multiple ErrorRecord', () => { - // When - const publicNames = PlaceholderService.extractClassesPublicNames(errorRecords); - - // Then - expect(publicNames).toEqual('Placeholder service, Data load api service'); - }); - }); + fixture = TestBed.createComponent(TestComponentStub); + + gridDebugElement = fixture.debugElement.query( + By.directive(ClarityDataGridComponentStub), + ); + gridComponent = gridDebugElement.componentInstance; + + placeholderDebugElement = fixture.debugElement.query( + By.directive(PlaceholderComponentStub), + ); + placeholderComponent = placeholderDebugElement.componentInstance; + + renderer2 = placeholderComponent.renderer2; + + service = placeholderDebugElement.injector.get(PlaceholderService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + describe("Statics::", () => { + describe("Methods::", () => { + let errorCodes: string[]; + let errorRecords: ErrorRecord[]; + + beforeEach(() => { + errorCodes = [ + generateErrorCode( + PlaceholderService.CLASS_NAME, + PlaceholderService.PUBLIC_NAME, + "refineElementsState", + "500", + ), + generateErrorCode( + PlaceholderService.CLASS_NAME, + PlaceholderService.PUBLIC_NAME, + "refineElementsState", + "503", + ), + generateErrorCode( + "DataLoadApiService", + "Data-Load-Api-Service", + "loadData", + "422", + ), + ]; + errorRecords = [ + { + code: errorCodes[0], + error: new HttpErrorResponse({ + error: new Error("Some Error 1"), + status: 500, + }), + httpStatusCode: 500, + objectUUID: "SomeObjectUUID_1", + }, + { + code: errorCodes[1], + error: new HttpErrorResponse({ + error: new Error("Some Error 2"), + status: 503, + }), + httpStatusCode: 503, + objectUUID: "SomeObjectUUID_1", + }, + { + code: errorCodes[2], + error: new HttpErrorResponse({ + error: new Error("Some Error 3"), + status: 422, + }), + httpStatusCode: 422, + objectUUID: "SomeObjectUUID_2", + }, + ]; + }); + + describe("|extractClassPublicName|", () => { + it("should verify will extract Class Public Name", () => { + // When + const publicName1 = PlaceholderService.extractClassPublicName( + errorRecords[0], + ); + const publicName2 = PlaceholderService.extractClassPublicName( + errorRecords[1], + ); + const publicName3 = PlaceholderService.extractClassPublicName( + errorRecords[2], + ); + const publicName4 = PlaceholderService.extractClassPublicName(null); + + // Then + expect(publicName1).toEqual("Placeholder service"); + expect(publicName2).toEqual("Placeholder service"); + expect(publicName3).toEqual("Data load api service"); + expect(publicName4).toEqual(""); }); + }); + + describe("|extractClassesPublicNames|", () => { + it("should verify will extract Class Public Names of multiple ErrorRecord", () => { + // When + const publicNames = + PlaceholderService.extractClassesPublicNames(errorRecords); + + // Then + expect(publicNames).toEqual( + "Placeholder service, Data load api service", + ); + }); + }); }); + }); + + describe("Methods::", () => { + describe("|refineElementsState|", () => { + it("should verify method would be invoked from stub Component upon change detection", () => { + // Given + const spyMethod = spyOn( + service, + "refineElementsState", + ).and.callThrough(); + + // When + fixture.detectChanges(); + + // Then + expect(spyMethod).toHaveBeenCalledWith( + placeholderComponent.elementRef, + false, + ); + }); - describe('Methods::', () => { - describe('|refineElementsState|', () => { - it('should verify method would be invoked from stub Component upon change detection', () => { - // Given - const spyMethod = spyOn(service, 'refineElementsState').and.callThrough(); - - // When - fixture.detectChanges(); - - // Then - expect(spyMethod).toHaveBeenCalledWith(placeholderComponent.elementRef, false); - }); - - it('should verify will append styling in head', fakeAsync(() => { - // When - fixture.detectChanges(); + it("should verify will append styling in head", fakeAsync(() => { + // When + fixture.detectChanges(); - tick(2000); + tick(2000); - // Then - const styleElementGrid = document.querySelector(`head style[data-shared-grid-style="${randomStringGrid}"]`); - const styleElementStandalone = document.querySelector( - `head style[data-shared-placeholder-style="${randomStringStandalone}"]` - ); + // Then + const styleElementGrid = document.querySelector( + `head style[data-shared-grid-style="${randomStringGrid}"]`, + ); + const styleElementStandalone = document.querySelector( + `head style[data-shared-placeholder-style="${randomStringStandalone}"]`, + ); - expect(styleElementGrid).toBeDefined(); - expect(styleElementGrid.innerHTML.trim().replace(/\s/g, '')).toEqual( - ` + expect(styleElementGrid).toBeDefined(); + expect(styleElementGrid.innerHTML.trim().replace(/\s/g, "")).toEqual( + ` clr-datagrid[data-shared-grid="${randomStringGrid}"] clr-dg-placeholder .datagrid-placeholder.datagrid-empty { justify-content: center; } @@ -244,66 +305,75 @@ describe('PlaceholderService', () => { display: block; } ` - .trim() - .replace(/\s/g, '') - ); + .trim() + .replace(/\s/g, ""), + ); - expect(styleElementStandalone).toBeNull(); - })); + expect(styleElementStandalone).toBeNull(); + })); - it('should verify wont append styling in head if grid is not found', fakeAsync(() => { - // Given - const spyRenderer2ParentNode = spyOn(renderer2, 'parentNode').and.returnValue(null); + it("should verify wont append styling in head if grid is not found", fakeAsync(() => { + // Given + const spyRenderer2ParentNode = spyOn( + renderer2, + "parentNode", + ).and.returnValue(null); - // When - fixture.detectChanges(); + // When + fixture.detectChanges(); - tick(8000); + tick(8000); - // Then - const styleElementGrid = document.querySelector(`head style[data-shared-grid-style="${randomStringGrid}"]`); - expect(styleElementGrid).toEqual(null); - expect(spyRenderer2ParentNode).toHaveBeenCalledTimes(200); - }), 10000); + // Then + const styleElementGrid = document.querySelector( + `head style[data-shared-grid-style="${randomStringGrid}"]`, + ); + expect(styleElementGrid).toEqual(null); + expect(spyRenderer2ParentNode).toHaveBeenCalledTimes(200); + }), 10000); - it('should verify will append styling in head if element is standalone', fakeAsync(() => { - // Given - spyOn(renderer2, 'parentNode').and.returnValue(null); + it("should verify will append styling in head if element is standalone", fakeAsync(() => { + // Given + spyOn(renderer2, "parentNode").and.returnValue(null); - // When - fixture.detectChanges(); + // When + fixture.detectChanges(); - tick(8000); + tick(8000); - // Then - const styleElementStandalone = document.querySelector( - `head style[data-shared-placeholder-style="${randomStringStandalone}"]` - ); + // Then + const styleElementStandalone = document.querySelector( + `head style[data-shared-placeholder-style="${randomStringStandalone}"]`, + ); - expect(styleElementStandalone).toBeDefined(); - expect(styleElementStandalone.innerHTML.trim().replace(/\s/g, '')).toEqual( - ` + expect(styleElementStandalone).toBeDefined(); + expect( + styleElementStandalone.innerHTML.trim().replace(/\s/g, ""), + ).toEqual( + ` shared-placeholder[data-shared-placeholder="${randomStringStandalone}"] { margin-top: 5rem; } ` - .trim() - .replace(/\s/g, '') - ); - }), 10000); + .trim() + .replace(/\s/g, ""), + ); + }), 10000); - it('should verify will toggle appended styling in head depending of provided flag to method', fakeAsync(() => { - // When 1 - fixture.detectChanges(); + it("should verify will toggle appended styling in head depending of provided flag to method", fakeAsync(() => { + // When 1 + fixture.detectChanges(); - tick(2000); + tick(2000); - // Then 1 - const styleElement = document.querySelector(`head style[data-shared-grid-style="${randomStringGrid}"]`); + // Then 1 + const styleElement = document.querySelector( + `head style[data-shared-grid-style="${randomStringGrid}"]`, + ); - expect(styleElement).toBeDefined(); - expect(styleElement.innerHTML.trim().replace(/\s/g, '')).toEqual( - ` + expect(styleElement).toBeDefined(); + expect(styleElement.innerHTML.trim().replace(/\s/g, "")).toEqual( + ` clr-datagrid[data-shared-grid="${randomStringGrid}"] clr-dg-placeholder .datagrid-placeholder.datagrid-empty { justify-content: center; } @@ -311,18 +381,21 @@ describe('PlaceholderService', () => { display: block; } ` - .trim() - .replace(/\s/g, '') - ); + .trim() + .replace(/\s/g, ""), + ); - // When 2 - placeholderDebugElement.triggerEventHandler('changeHideDefaultStateImageInGrid', { value: true }); + // When 2 + placeholderDebugElement.triggerEventHandler( + "changeHideDefaultStateImageInGrid", + { value: true }, + ); - tick(2000); + tick(2000); - // Then 2 - expect(styleElement.innerHTML.trim().replace(/\s/g, '')).toEqual( - ` + // Then 2 + expect(styleElement.innerHTML.trim().replace(/\s/g, "")).toEqual( + ` clr-datagrid[data-shared-grid="${randomStringGrid}"] clr-dg-placeholder .datagrid-placeholder.datagrid-empty { justify-content: center; } @@ -330,46 +403,65 @@ describe('PlaceholderService', () => { display: none; } ` - .trim() - .replace(/\s/g, '') - ); - })); - - it('should verify will append data attribute to grid component when found', fakeAsync(() => { - // When - fixture.detectChanges(); - - tick(2000); - - // Then - expect(gridComponent.elementRef.nativeElement.hasAttribute('data-shared-grid')).toBeTrue(); - expect(gridComponent.elementRef.nativeElement.getAttribute('data-shared-grid')).toEqual(randomStringGrid); - })); - - it('should verify wont append data attribute to grid component if it cannot find', fakeAsync(() => { - // Given - const spyRenderer2ParentNode = spyOn(renderer2, 'parentNode').and.returnValue(null); - - // When - fixture.detectChanges(); - - tick(8000); - - // Then - expect(gridComponent.elementRef.nativeElement.hasAttribute('data-shared-grid')).toBeFalse(); - expect(spyRenderer2ParentNode).toHaveBeenCalledTimes(200); - }), 10000); - - it('should verify wont append data attribute to grid component if it cannot find due to greater placeholder depth of 15', fakeAsync(() => { - // Given - TestBed.resetTestingModule() - .configureTestingModule({ - declarations: [TestComponentStub, PlaceholderComponentStub, ClarityDataGridComponentStub], - providers: [Renderer2] - }) - .overrideComponent(ClarityDataGridComponentStub, { - set: { - template: ` + .trim() + .replace(/\s/g, ""), + ); + })); + + it("should verify will append data attribute to grid component when found", fakeAsync(() => { + // When + fixture.detectChanges(); + + tick(2000); + + // Then + expect( + gridComponent.elementRef.nativeElement.hasAttribute( + "data-shared-grid", + ), + ).toBeTrue(); + expect( + gridComponent.elementRef.nativeElement.getAttribute( + "data-shared-grid", + ), + ).toEqual(randomStringGrid); + })); + + it("should verify wont append data attribute to grid component if it cannot find", fakeAsync(() => { + // Given + const spyRenderer2ParentNode = spyOn( + renderer2, + "parentNode", + ).and.returnValue(null); + + // When + fixture.detectChanges(); + + tick(8000); + + // Then + expect( + gridComponent.elementRef.nativeElement.hasAttribute( + "data-shared-grid", + ), + ).toBeFalse(); + expect(spyRenderer2ParentNode).toHaveBeenCalledTimes(200); + }), 10000); + + it("should verify wont append data attribute to grid component if it cannot find due to greater placeholder depth of 15", fakeAsync(() => { + // Given + TestBed.resetTestingModule() + .configureTestingModule({ + declarations: [ + TestComponentStub, + PlaceholderComponentStub, + ClarityDataGridComponentStub, + ], + providers: [Renderer2], + }) + .overrideComponent(ClarityDataGridComponentStub, { + set: { + template: `
    @@ -401,137 +493,155 @@ describe('PlaceholderService', () => {
    - ` - } - }); + `, + }, + }); - // When - fixture = TestBed.createComponent(TestComponentStub); - - gridDebugElement = fixture.debugElement.query(By.directive(ClarityDataGridComponentStub)); - gridComponent = gridDebugElement.componentInstance; - - fixture.detectChanges(); - - tick(2000); + // When + fixture = TestBed.createComponent(TestComponentStub); - // Then - expect(gridComponent.elementRef.nativeElement.hasAttribute('data-shared-grid')).toBeFalse(); - })); + gridDebugElement = fixture.debugElement.query( + By.directive(ClarityDataGridComponentStub), + ); + gridComponent = gridDebugElement.componentInstance; - it('should verify will append data attribute to placeholder when is standalone', fakeAsync(() => { - // Given - TestBed.resetTestingModule() - .configureTestingModule({ - declarations: [PlaceholderComponentStub, StandaloneComponentStub, TestComponentStub], - providers: [Renderer2] - }) - .overrideComponent(TestComponentStub, { - set: { - template: ` + fixture.detectChanges(); + + tick(2000); + + // Then + expect( + gridComponent.elementRef.nativeElement.hasAttribute( + "data-shared-grid", + ), + ).toBeFalse(); + })); + + it("should verify will append data attribute to placeholder when is standalone", fakeAsync(() => { + // Given + TestBed.resetTestingModule() + .configureTestingModule({ + declarations: [ + PlaceholderComponentStub, + StandaloneComponentStub, + TestComponentStub, + ], + providers: [Renderer2], + }) + .overrideComponent(TestComponentStub, { + set: { + template: `
    - ` - } - }); - - // When - fixture = TestBed.createComponent(TestComponentStub); + `, + }, + }); - placeholderDebugElement = fixture.debugElement.query(By.directive(PlaceholderComponentStub)); - placeholderComponent = placeholderDebugElement.componentInstance; - - fixture.detectChanges(); + // When + fixture = TestBed.createComponent(TestComponentStub); - tick(4000); + placeholderDebugElement = fixture.debugElement.query( + By.directive(PlaceholderComponentStub), + ); + placeholderComponent = placeholderDebugElement.componentInstance; - // Then - expect(placeholderComponent.elementRef.nativeElement.hasAttribute('data-shared-placeholder')).toBeTrue(); - expect(placeholderComponent.elementRef.nativeElement.getAttribute('data-shared-placeholder')).toEqual( - randomStringStandalone - ); - })); - }); + fixture.detectChanges(); + + tick(4000); + + // Then + expect( + placeholderComponent.elementRef.nativeElement.hasAttribute( + "data-shared-placeholder", + ), + ).toBeTrue(); + expect( + placeholderComponent.elementRef.nativeElement.getAttribute( + "data-shared-placeholder", + ), + ).toEqual(randomStringStandalone); + })); + }); - describe('|extractErrorInformation|', () => { - describe('parameterized_test', () => { - const errors: HttpErrorResponse[] = [ - new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: `Something bad happened and it's string` - }), - new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: { - what: `text what`, - why: `text why`, - consequences: `text consequences`, - countermeasures: `text countermeasures` - } - }), - new HttpErrorResponse({ - status: HttpStatusCode.InternalServerError, - error: null - }), - new HttpErrorResponse({ - status: HttpStatusCode.BadGateway, - error: undefined - }), - new SyntaxError('Unsupported Action') as HttpErrorResponse - ]; - const params: Array<[string, Error, ApiErrorMessage]> = [ - [ - 'error is HttpErrorResponse and nested error is string', - errors[0], - { what: `${errors[0].error}`, why: `${errors[0].message}` } - ], - [ - 'error is HttpErrorResponse and nested error is formatted ApiErrorMessage', - errors[1], - { - what: `${errors[1].error.what}`, - why: `${errors[1].error.why}`, - consequences: `${errors[1].error.consequences}`, - countermeasures: `${errors[1].error.countermeasures}` - } - ], - [ - 'error is HttpErrorResponse and nested error is null', - errors[2], - { - what: 'Please contact Superollider and report the issue', - why: 'Internal Server Error' - } - ], - [ - 'error is HttpErrorResponse and nested error is undefined', - errors[3], - { - what: 'Please contact Superollider and report the issue', - why: 'Unknown Error' - } - ], - [ - 'error is not HttpErrorResponse', - errors[4], - { - what: 'Please contact Superollider and report the issue', - why: 'Unknown Error' - } - ] - ]; - - for (const [description, error, assertion] of params) { - it(`should verify will return ApiErrorMessage when ${description}`, () => { - // When - const apiMessage = service.extractErrorInformation(error); - - // Then - expect(apiMessage).toEqual(assertion); - }); - } - }); - }); + describe("|extractErrorInformation|", () => { + describe("parameterized_test", () => { + const errors: HttpErrorResponse[] = [ + new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: `Something bad happened and it's string`, + }), + new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: { + what: `text what`, + why: `text why`, + consequences: `text consequences`, + countermeasures: `text countermeasures`, + }, + }), + new HttpErrorResponse({ + status: HttpStatusCode.InternalServerError, + error: null, + }), + new HttpErrorResponse({ + status: HttpStatusCode.BadGateway, + error: undefined, + }), + new SyntaxError("Unsupported Action") as HttpErrorResponse, + ]; + const params: Array<[string, Error, ApiErrorMessage]> = [ + [ + "error is HttpErrorResponse and nested error is string", + errors[0], + { what: `${errors[0].error}`, why: `${errors[0].message}` }, + ], + [ + "error is HttpErrorResponse and nested error is formatted ApiErrorMessage", + errors[1], + { + what: `${errors[1].error.what}`, + why: `${errors[1].error.why}`, + consequences: `${errors[1].error.consequences}`, + countermeasures: `${errors[1].error.countermeasures}`, + }, + ], + [ + "error is HttpErrorResponse and nested error is null", + errors[2], + { + what: "Please contact Superollider and report the issue", + why: "Internal Server Error", + }, + ], + [ + "error is HttpErrorResponse and nested error is undefined", + errors[3], + { + what: "Please contact Superollider and report the issue", + why: "Unknown Error", + }, + ], + [ + "error is not HttpErrorResponse", + errors[4], + { + what: "Please contact Superollider and report the issue", + why: "Unknown Error", + }, + ], + ]; + + for (const [description, error, assertion] of params) { + it(`should verify will return ApiErrorMessage when ${description}`, () => { + // When + const apiMessage = service.extractErrorInformation(error); + + // Then + expect(apiMessage).toEqual(assertion); + }); + } + }); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.ts index 85fb0c88a9..bc0f57928e 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/placeholder/services/placeholder.service.ts @@ -3,305 +3,340 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ElementRef, Injectable, OnDestroy, Renderer2 } from '@angular/core'; +import { ElementRef, Injectable, OnDestroy, Renderer2 } from "@angular/core"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { ApiErrorMessage, ErrorRecord, TaurusObject } from '../../../common'; +import { ApiErrorMessage, ErrorRecord, TaurusObject } from "../../../common"; -import { getApiFormattedErrorMessage } from '../../../core'; +import { getApiFormattedErrorMessage } from "../../../core"; @Injectable() export class PlaceholderService extends TaurusObject implements OnDestroy { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'PlaceholderService'; - - /** - * @inheritDoc - */ - static override readonly PUBLIC_NAME: string = 'Placeholder-Service'; - - // lookup flags - private _lookupInProgress = false; - private _isGridParentFound = false; - private _finderLookupTimeoutRef: number; - - // styling elements - private _headElement: HTMLHeadElement; - private _gridStyleElement: HTMLStyleElement; - private _standalonePlaceholderStyleElement: HTMLElement; - - // utility - private readonly _gridRandomAttribute: string; - private readonly _standalonePlaceholderRandomAttribute: string; - - // temporary storage - private _elementRef: ElementRef; - private _hideDefaultEmptyStateImageInGrid = false; - - /** - * ** Constructor. - */ - constructor(private readonly renderer2: Renderer2) { - super(PlaceholderService.CLASS_NAME); - - this._gridRandomAttribute = CollectionsUtil.generateRandomString(); - this._standalonePlaceholderRandomAttribute = CollectionsUtil.generateRandomString(); - } - - /** - * ** Extract public name of classes from multiple error records. - */ - static extractClassesPublicNames(errorRecords: ErrorRecord[]): string { - const publicNames = errorRecords - .map((r) => PlaceholderService.extractClassPublicName(r)) - .filter((publicName) => CollectionsUtil.isString(publicName) && publicName.length > 0); - - return CollectionsUtil.uniqueArray(publicNames).join(', '); - } - - /** - * ** Extract class public name from provided error record. - */ - static extractClassPublicName(errorRecord: ErrorRecord): string { - if (errorRecord) { - if (errorRecord.code && errorRecord.code.length > 0) { - /** - * class public name is second with underscore following pattern described in {@link ErrorRecord.code} - */ - const codeChunks = errorRecord.code.split('_'); - - if (codeChunks.length >= 3) { - const publicName = codeChunks[1]; - - if (publicName && publicName.length > 2) { - const publicNameNormalized = publicName.replace(/-/g, ' '); - - return publicNameNormalized.substring(0, 1).toUpperCase() + publicNameNormalized.substring(1).toLowerCase(); - } - } - } + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "PlaceholderService"; + + /** + * @inheritDoc + */ + static override readonly PUBLIC_NAME: string = "Placeholder-Service"; + + // lookup flags + private _lookupInProgress = false; + private _isGridParentFound = false; + private _finderLookupTimeoutRef: number; + + // styling elements + private _headElement: HTMLHeadElement; + private _gridStyleElement: HTMLStyleElement; + private _standalonePlaceholderStyleElement: HTMLElement; + + // utility + private readonly _gridRandomAttribute: string; + private readonly _standalonePlaceholderRandomAttribute: string; + + // temporary storage + private _elementRef: ElementRef; + private _hideDefaultEmptyStateImageInGrid = false; + + /** + * ** Constructor. + */ + constructor(private readonly renderer2: Renderer2) { + super(PlaceholderService.CLASS_NAME); + + this._gridRandomAttribute = CollectionsUtil.generateRandomString(); + this._standalonePlaceholderRandomAttribute = + CollectionsUtil.generateRandomString(); + } + + /** + * ** Extract public name of classes from multiple error records. + */ + static extractClassesPublicNames(errorRecords: ErrorRecord[]): string { + const publicNames = errorRecords + .map((r) => PlaceholderService.extractClassPublicName(r)) + .filter( + (publicName) => + CollectionsUtil.isString(publicName) && publicName.length > 0, + ); + + return CollectionsUtil.uniqueArray(publicNames).join(", "); + } + + /** + * ** Extract class public name from provided error record. + */ + static extractClassPublicName(errorRecord: ErrorRecord): string { + if (errorRecord) { + if (errorRecord.code && errorRecord.code.length > 0) { + /** + * class public name is second with underscore following pattern described in {@link ErrorRecord.code} + */ + const codeChunks = errorRecord.code.split("_"); + + if (codeChunks.length >= 3) { + const publicName = codeChunks[1]; + + if (publicName && publicName.length > 2) { + const publicNameNormalized = publicName.replace(/-/g, " "); + + return ( + publicNameNormalized.substring(0, 1).toUpperCase() + + publicNameNormalized.substring(1).toLowerCase() + ); + } } - - return ''; + } } - /** - * ** Refines elements state and their corresponding styles. - */ - refineElementsState(elementRef: ElementRef, hideDefaultEmptyStateImageInGrid: boolean): void { - if (CollectionsUtil.isDefined(elementRef)) { - this._elementRef = elementRef; - } - - if (CollectionsUtil.isDefined(hideDefaultEmptyStateImageInGrid)) { - this._hideDefaultEmptyStateImageInGrid = hideDefaultEmptyStateImageInGrid; - } - - if (this._lookupInProgress) { - return; - } - - if (!this._isGridParentFound) { - this._lookupInProgress = true; - - this._findHeadElement(); - - this._findPlaceholderParentGrid() - .then((placeholderGridParent) => { - this._isGridParentFound = !!placeholderGridParent; - - this._removeStandalonePlaceholderStyle(); - - this._appendGridPlaceholderStyle(); - this._addGridDataAttribute(placeholderGridParent); - this._toggleGridPlaceholderStyle(); - }) - .catch((_error) => { - this._isGridParentFound = false; - - this._removeGridPlaceholderStyle(); - - this._appendStandalonePlaceholderStyle(); - this._addStandalonePlaceholderDataAttribute(); - this._toggleStandalonePlaceholderStyle(); - }) - .finally(() => { - this._lookupInProgress = false; - }); - } else { - this._toggleGridPlaceholderStyle(); - this._toggleStandalonePlaceholderStyle(); - } + return ""; + } + + /** + * ** Refines elements state and their corresponding styles. + */ + refineElementsState( + elementRef: ElementRef, + hideDefaultEmptyStateImageInGrid: boolean, + ): void { + if (CollectionsUtil.isDefined(elementRef)) { + this._elementRef = elementRef; } - /** - * ** Get API formatted error message from provided Error. - */ - extractErrorInformation(error: Error): ApiErrorMessage { - return getApiFormattedErrorMessage(error); + if (CollectionsUtil.isDefined(hideDefaultEmptyStateImageInGrid)) { + this._hideDefaultEmptyStateImageInGrid = hideDefaultEmptyStateImageInGrid; } - /** - * @inheritDoc - */ - override ngOnDestroy(): void { - if (this._finderLookupTimeoutRef) { - window.clearTimeout(this._finderLookupTimeoutRef); - } - - this._removeGridPlaceholderStyle(); - this._removeStandalonePlaceholderStyle(); - - super.ngOnDestroy(); + if (this._lookupInProgress) { + return; } - private _findPlaceholderParentGrid(): Promise { - let parentElementFinderAttempts = 0; + if (!this._isGridParentFound) { + this._lookupInProgress = true; - let resolveRef: (value: HTMLElement) => void; - let rejectRef: (reason: string) => void; + this._findHeadElement(); - const parentFinder = () => { - parentElementFinderAttempts++; + this._findPlaceholderParentGrid() + .then((placeholderGridParent) => { + this._isGridParentFound = !!placeholderGridParent; - const firstLevelParent: HTMLElement = this.renderer2.parentNode(this._elementRef.nativeElement) as HTMLElement; - if (firstLevelParent) { - const foundParentGrid = this._traverseToFindParentGrid(firstLevelParent); + this._removeStandalonePlaceholderStyle(); - if (foundParentGrid) { - resolveRef(foundParentGrid); - } else { - rejectRef('Cannot find parent grid (clr-datagrid)!'); - } - } else if (parentElementFinderAttempts < 200) { - this._finderLookupTimeoutRef = window.setTimeout(() => { - this._finderLookupTimeoutRef = null; + this._appendGridPlaceholderStyle(); + this._addGridDataAttribute(placeholderGridParent); + this._toggleGridPlaceholderStyle(); + }) + .catch((_error) => { + this._isGridParentFound = false; - parentFinder(); - }, 25); - } else { - rejectRef('Cannot find parent grid (clr-datagrid)!'); - } - }; + this._removeGridPlaceholderStyle(); - return new Promise((resolve, reject) => { - resolveRef = resolve; - rejectRef = reject; - - parentFinder(); + this._appendStandalonePlaceholderStyle(); + this._addStandalonePlaceholderDataAttribute(); + this._toggleStandalonePlaceholderStyle(); + }) + .finally(() => { + this._lookupInProgress = false; }); + } else { + this._toggleGridPlaceholderStyle(); + this._toggleStandalonePlaceholderStyle(); + } + } + + /** + * ** Get API formatted error message from provided Error. + */ + extractErrorInformation(error: Error): ApiErrorMessage { + return getApiFormattedErrorMessage(error); + } + + /** + * @inheritDoc + */ + override ngOnDestroy(): void { + if (this._finderLookupTimeoutRef) { + window.clearTimeout(this._finderLookupTimeoutRef); } - private _traverseToFindParentGrid(element: HTMLElement): HTMLElement { - let loop = 0; - let parentElement: HTMLElement = element; + this._removeGridPlaceholderStyle(); + this._removeStandalonePlaceholderStyle(); - while (loop < 15) { - if (parentElement) { - loop++; + super.ngOnDestroy(); + } - if (!parentElement.tagName) { - break; - } + private _findPlaceholderParentGrid(): Promise { + let parentElementFinderAttempts = 0; - if (parentElement.tagName.toLowerCase() === 'clr-datagrid') { - return parentElement; - } + let resolveRef: (value: HTMLElement) => void; + let rejectRef: (reason: string) => void; - parentElement = this.renderer2.parentNode(parentElement) as HTMLElement; - } else { - break; - } - } + const parentFinder = () => { + parentElementFinderAttempts++; - return null; - } + const firstLevelParent: HTMLElement = this.renderer2.parentNode( + this._elementRef.nativeElement, + ) as HTMLElement; + if (firstLevelParent) { + const foundParentGrid = + this._traverseToFindParentGrid(firstLevelParent); - private _findHeadElement(): void { - if (this._headElement) { - return; + if (foundParentGrid) { + resolveRef(foundParentGrid); + } else { + rejectRef("Cannot find parent grid (clr-datagrid)!"); + } + } else if (parentElementFinderAttempts < 200) { + this._finderLookupTimeoutRef = window.setTimeout(() => { + this._finderLookupTimeoutRef = null; + + parentFinder(); + }, 25); + } else { + rejectRef("Cannot find parent grid (clr-datagrid)!"); + } + }; + + return new Promise((resolve, reject) => { + resolveRef = resolve; + rejectRef = reject; + + parentFinder(); + }); + } + + private _traverseToFindParentGrid(element: HTMLElement): HTMLElement { + let loop = 0; + let parentElement: HTMLElement = element; + + while (loop < 15) { + if (parentElement) { + loop++; + + if (!parentElement.tagName) { + break; } - this._headElement = document.querySelector('head'); + if (parentElement.tagName.toLowerCase() === "clr-datagrid") { + return parentElement; + } + + parentElement = this.renderer2.parentNode(parentElement) as HTMLElement; + } else { + break; + } } - private _appendGridPlaceholderStyle(): void { - if (!this._gridStyleElement) { - this._gridStyleElement = this.renderer2.createElement('style') as HTMLStyleElement; + return null; + } - this.renderer2.setAttribute(this._gridStyleElement, 'data-shared-grid-style', this._gridRandomAttribute); - this.renderer2.appendChild(this._headElement, this._gridStyleElement); - } + private _findHeadElement(): void { + if (this._headElement) { + return; } - private _appendStandalonePlaceholderStyle(): void { - if (!this._standalonePlaceholderStyleElement) { - this._standalonePlaceholderStyleElement = this.renderer2.createElement('style') as HTMLStyleElement; + this._headElement = document.querySelector("head"); + } + + private _appendGridPlaceholderStyle(): void { + if (!this._gridStyleElement) { + this._gridStyleElement = this.renderer2.createElement( + "style", + ) as HTMLStyleElement; + + this.renderer2.setAttribute( + this._gridStyleElement, + "data-shared-grid-style", + this._gridRandomAttribute, + ); + this.renderer2.appendChild(this._headElement, this._gridStyleElement); + } + } + + private _appendStandalonePlaceholderStyle(): void { + if (!this._standalonePlaceholderStyleElement) { + this._standalonePlaceholderStyleElement = this.renderer2.createElement( + "style", + ) as HTMLStyleElement; + + this.renderer2.setAttribute( + this._standalonePlaceholderStyleElement, + "data-shared-placeholder-style", + this._standalonePlaceholderRandomAttribute, + ); + this.renderer2.appendChild( + this._headElement, + this._standalonePlaceholderStyleElement, + ); + } + } - this.renderer2.setAttribute( - this._standalonePlaceholderStyleElement, - 'data-shared-placeholder-style', - this._standalonePlaceholderRandomAttribute - ); - this.renderer2.appendChild(this._headElement, this._standalonePlaceholderStyleElement); - } + private _addGridDataAttribute(element: HTMLElement): void { + if (!element) { + return; } - private _addGridDataAttribute(element: HTMLElement): void { - if (!element) { - return; - } + this.renderer2.setAttribute( + element, + "data-shared-grid", + this._gridRandomAttribute, + ); + } - this.renderer2.setAttribute(element, 'data-shared-grid', this._gridRandomAttribute); + private _addStandalonePlaceholderDataAttribute(): void { + if (!this._elementRef.nativeElement) { + return; } - private _addStandalonePlaceholderDataAttribute(): void { - if (!this._elementRef.nativeElement) { - return; - } + this.renderer2.setAttribute( + this._elementRef.nativeElement, + "data-shared-placeholder", + this._standalonePlaceholderRandomAttribute, + ); + } - this.renderer2.setAttribute(this._elementRef.nativeElement, 'data-shared-placeholder', this._standalonePlaceholderRandomAttribute); + private _toggleGridPlaceholderStyle(): void { + if (!this._gridStyleElement) { + return; } - private _toggleGridPlaceholderStyle(): void { - if (!this._gridStyleElement) { - return; - } - - this._gridStyleElement.innerHTML = ` + this._gridStyleElement.innerHTML = ` clr-datagrid[data-shared-grid="${this._gridRandomAttribute}"] clr-dg-placeholder .datagrid-placeholder.datagrid-empty { justify-content: center; } clr-datagrid[data-shared-grid="${this._gridRandomAttribute}"] clr-dg-placeholder .datagrid-placeholder-image { - display: ${this._hideDefaultEmptyStateImageInGrid ? 'none' : 'block'}; + display: ${this._hideDefaultEmptyStateImageInGrid ? "none" : "block"}; } `; - } + } - private _toggleStandalonePlaceholderStyle(): void { - if (!this._standalonePlaceholderStyleElement) { - return; - } + private _toggleStandalonePlaceholderStyle(): void { + if (!this._standalonePlaceholderStyleElement) { + return; + } - this._standalonePlaceholderStyleElement.innerHTML = ` + this._standalonePlaceholderStyleElement.innerHTML = ` shared-placeholder[data-shared-placeholder="${this._standalonePlaceholderRandomAttribute}"] { margin-top: 5rem; } `; - } + } - private _removeGridPlaceholderStyle(): void { - if (this._gridStyleElement) { - this.renderer2.removeChild(this._headElement, this._gridStyleElement); - } + private _removeGridPlaceholderStyle(): void { + if (this._gridStyleElement) { + this.renderer2.removeChild(this._headElement, this._gridStyleElement); } - - private _removeStandalonePlaceholderStyle(): void { - if (this._standalonePlaceholderStyleElement) { - this.renderer2.removeChild(this._headElement, this._standalonePlaceholderStyleElement); - } + } + + private _removeStandalonePlaceholderStyle(): void { + if (this._standalonePlaceholderStyleElement) { + this.renderer2.removeChild( + this._headElement, + this._standalonePlaceholderStyleElement, + ); } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts index 2082c7b2d2..b709990fc8 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './_model/public-api'; -export * from './confirmation/public-api'; -export * from './directives/public-api'; -export * from './dynamic-components/public-api'; -export * from './error-handler/public-api'; -export * from './pipes/public-api'; -export * from './placeholder/public-api'; -export * from './toasts/public-api'; -export * from './url-opener/public-api'; -export * from './warning/public-api'; +export * from "./_model/public-api"; +export * from "./confirmation/public-api"; +export * from "./directives/public-api"; +export * from "./dynamic-components/public-api"; +export * from "./error-handler/public-api"; +export * from "./pipes/public-api"; +export * from "./placeholder/public-api"; +export * from "./toasts/public-api"; +export * from "./url-opener/public-api"; +export * from "./warning/public-api"; // export Features module -export * from './vdk-shared-features.module'; +export * from "./vdk-shared-features.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts index ddb649d7b5..7cf90eca12 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './toast.interface'; +export * from "./toast.interface"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts index fe3fffbcbd..957346ffba 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts @@ -3,27 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpErrorResponse } from "@angular/common/http"; -import { VmwToastType } from '../../../commons'; -import { ApiErrorMessage } from '../../../common'; +import { VmwToastType } from "../../../commons"; +import { ApiErrorMessage } from "../../../common"; export interface Toast { + title: string; + description: string; + type: VmwToastType; + error?: Error | ApiErrorMessage | FormattedError | HttpErrorResponse; + expanded?: boolean; + responseStatus?: number; + extendedData?: { title: string; description: string; - type: VmwToastType; - error?: Error | ApiErrorMessage | FormattedError | HttpErrorResponse; - expanded?: boolean; - responseStatus?: number; - extendedData?: { - title: string; - description: string; - }; + }; } export interface FormattedError { - consequences?: string; - countermeasures?: string; - what: string; - why: string; + consequences?: string; + countermeasures?: string; + what: string; + why: string; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts index cb9b8caa77..1a7730226f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { Toast, FormattedError } from './model'; -export { ToastService } from './service'; -export { ToastsComponent } from './widget'; -export { ToastsModule } from './toasts.module'; +export { Toast, FormattedError } from "./model"; +export { ToastService } from "./service"; +export { ToastsComponent } from "./widget"; +export { ToastsModule } from "./toasts.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts index fe1df37233..75116eadf6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './toast.service'; +export * from "./toast.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts index 37266a6e61..a6be0e02a9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts @@ -3,37 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TestBed } from '@angular/core/testing'; +import { TestBed } from "@angular/core/testing"; -import { VmwToastType } from '../../../commons'; +import { VmwToastType } from "../../../commons"; -import { ToastService } from './toast.service'; +import { ToastService } from "./toast.service"; -describe('ToastServiceComponent', () => { - let service: ToastService; +describe("ToastServiceComponent", () => { + let service: ToastService; - const TEST_TOAST = { - title: 'title001', - description: 'descr001', - type: VmwToastType.FAILURE - }; + const TEST_TOAST = { + title: "title001", + description: "descr001", + type: VmwToastType.FAILURE, + }; - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ToastService] - }); - service = TestBed.inject(ToastService); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ToastService], }); - - it('should create', () => { - expect(service).toBeTruthy(); - }); - - describe('show', () => { - it('makes expected calls', () => { - spyOn(service.notificationsSubject, 'next').and.callThrough(); - service.show(TEST_TOAST); - expect(service.notificationsSubject.next).toHaveBeenCalledWith(TEST_TOAST); - }); + service = TestBed.inject(ToastService); + }); + + it("should create", () => { + expect(service).toBeTruthy(); + }); + + describe("show", () => { + it("makes expected calls", () => { + spyOn(service.notificationsSubject, "next").and.callThrough(); + service.show(TEST_TOAST); + expect(service.notificationsSubject.next).toHaveBeenCalledWith( + TEST_TOAST, + ); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts index 4a2dd1c270..1c0e6dbcdf 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts @@ -3,28 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; -import { Subject } from 'rxjs'; +import { Subject } from "rxjs"; -import { Toast } from '../model'; +import { Toast } from "../model"; @Injectable() export class ToastService { - notificationsSubject = new Subject(); - private notification$ = this.notificationsSubject.asObservable(); + notificationsSubject = new Subject(); + private notification$ = this.notificationsSubject.asObservable(); - /** - * ** Get subscribable stream, that raise new Events when new Toast should be shown. - */ - getNotifications() { - return this.notification$; - } + /** + * ** Get subscribable stream, that raise new Events when new Toast should be shown. + */ + getNotifications() { + return this.notification$; + } - /** - * ** Show Toast message. - */ - show(toast: Toast) { - this.notificationsSubject.next(toast); - } + /** + * ** Show Toast message. + */ + show(toast: Toast) { + this.notificationsSubject.next(toast); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts index 848585e76d..5eca49397f 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts @@ -3,20 +3,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; -import { ClipboardModule } from 'ngx-clipboard'; +import { ClipboardModule } from "ngx-clipboard"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { VdkSharedComponentsModule } from '../../commons'; +import { VdkSharedComponentsModule } from "../../commons"; -import { ToastsComponent } from './widget'; +import { ToastsComponent } from "./widget"; @NgModule({ - imports: [CommonModule, ClarityModule, VdkSharedComponentsModule, ClipboardModule], - declarations: [ToastsComponent], - exports: [ToastsComponent] + imports: [ + CommonModule, + ClarityModule, + VdkSharedComponentsModule, + ClipboardModule, + ], + declarations: [ToastsComponent], + exports: [ToastsComponent], }) export class ToastsModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts index cffaed751e..e778e2bcbd 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './toasts.component'; +export * from "./toasts.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html index 415012fe75..370855edb4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html @@ -4,36 +4,36 @@ --> - -
    {{toast.title}}
    + +
    {{ toast.title }}
    -

    {{toast.description}}

    -

    - consequences: {{toast.error?.consequences}} -

    -

    - countermeasures: {{toast.error?.countermeasures}} -

    -

    - Please copy the details and report the error. -

    -
    +

    {{ toast.description }}

    +

    + consequences: {{ toast.error?.consequences }} +

    +

    + countermeasures: {{ toast.error?.countermeasures }} +

    +

    + Please copy the details and report the error. +

    +
    diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss index e2f0e3fc46..b79fc2fa64 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss @@ -4,14 +4,14 @@ */ .toast-container { - z-index: 1051; + z-index: 1051; - .toast-title { - max-width: 300px; - } + .toast-title { + max-width: 300px; + } - .toast-description { - margin-top: 5px; - max-width: 330px; - } + .toast-description { + margin-top: 5px; + max-width: 330px; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts index 0e47fc0d75..7e81bd5562 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts @@ -3,52 +3,52 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { VmwToastType } from '../../../commons'; - -import { ToastsComponent } from './toasts.component'; -import { ToastService } from '../service'; - -describe('ToastsComponent', () => { - let component: ToastsComponent; - let fixture: ComponentFixture; - - const TEST_TOAST = { - title: 'title001', - description: 'description001', - type: VmwToastType.FAILURE, - id: 10, - time: new Date() - }; - - const FIRST_ELEMENT_INDEX = 0; - const EMPTY_LIST_LENGTH = 0; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - providers: [ToastService], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - declarations: [ToastsComponent] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ToastsComponent); - component = fixture.componentInstance; - component.toasts = [TEST_TOAST]; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('remove', () => { - it('removes an element from the toasts list ', () => { - component.removeToast(FIRST_ELEMENT_INDEX); - expect(component.toasts.length).toEqual(EMPTY_LIST_LENGTH); - }); +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { VmwToastType } from "../../../commons"; + +import { ToastsComponent } from "./toasts.component"; +import { ToastService } from "../service"; + +describe("ToastsComponent", () => { + let component: ToastsComponent; + let fixture: ComponentFixture; + + const TEST_TOAST = { + title: "title001", + description: "description001", + type: VmwToastType.FAILURE, + id: 10, + time: new Date(), + }; + + const FIRST_ELEMENT_INDEX = 0; + const EMPTY_LIST_LENGTH = 0; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ToastService], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + declarations: [ToastsComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ToastsComponent); + component = fixture.componentInstance; + component.toasts = [TEST_TOAST]; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("remove", () => { + it("removes an element from the toasts list ", () => { + component.removeToast(FIRST_ELEMENT_INDEX); + expect(component.toasts.length).toEqual(EMPTY_LIST_LENGTH); }); + }); }); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts index cec0f89533..90096b7c2b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts @@ -3,155 +3,159 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit } from "@angular/core"; -import { ClipboardService } from 'ngx-clipboard'; +import { ClipboardService } from "ngx-clipboard"; -import { VmwToastType } from '../../../commons'; +import { VmwToastType } from "../../../commons"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { Toast } from '../model'; -import { ToastService } from '../service'; +import { Toast } from "../model"; +import { ToastService } from "../service"; interface ToastInternal extends Toast { - id: number; - time: Date; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error?: any; + id: number; + time: Date; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error?: any; } @Component({ - selector: 'shared-toasts', - templateUrl: './toasts.component.html', - styleUrls: ['./toasts.component.scss'] + selector: "shared-toasts", + templateUrl: "./toasts.component.html", + styleUrls: ["./toasts.component.scss"], }) export class ToastsComponent extends TaurusObject implements OnInit { - private static toastMessageCounter = 0; - - toasts: ToastInternal[]; - - constructor( - private readonly toastService: ToastService, - private readonly clipboardService: ClipboardService - ) { - super(); - this.toasts = []; - } - - /** - * ** Optimize Toast rendering using tracking with auto incremented ID per Toast. - */ - trackByRendering(_index: number, toast: ToastInternal): number { - return toast.id; - } - - /** - * ** Returns if Toast with given index is expanded. - */ - isToastExpanded(index: number): boolean { - return this.toasts[index].expanded; - } - - /** - * ** Evaluate if recommendation text for Copy and Report is visible. - */ - isReportRecommendationVisible(toast: ToastInternal, index: number): boolean { - return this.isToastExpanded(index) && this._isTypeError(toast) && toast.responseStatus !== 500; - } - - /** - * ** Remove Toast message. - */ - removeToast(index: number): void { - this.toasts.splice(index, 1); - } - - /** - * ** Toggle Toast expand details (expand/collapse). - */ - toggleToastExpandDetails(index: number): void { - this.toasts[index].expanded = !this.toasts[index].expanded; - } - - /** - * ** Copy to clipboard provided object. - */ - copyToClipboard(copy: ToastInternal): void { - try { - this.clipboardService.copy(JSON.stringify(copy)); - } catch (e) { - console.error(e); - - this._handleCopyActionError(); - } + private static toastMessageCounter = 0; + + toasts: ToastInternal[]; + + constructor( + private readonly toastService: ToastService, + private readonly clipboardService: ClipboardService, + ) { + super(); + this.toasts = []; + } + + /** + * ** Optimize Toast rendering using tracking with auto incremented ID per Toast. + */ + trackByRendering(_index: number, toast: ToastInternal): number { + return toast.id; + } + + /** + * ** Returns if Toast with given index is expanded. + */ + isToastExpanded(index: number): boolean { + return this.toasts[index].expanded; + } + + /** + * ** Evaluate if recommendation text for Copy and Report is visible. + */ + isReportRecommendationVisible(toast: ToastInternal, index: number): boolean { + return ( + this.isToastExpanded(index) && + this._isTypeError(toast) && + toast.responseStatus !== 500 + ); + } + + /** + * ** Remove Toast message. + */ + removeToast(index: number): void { + this.toasts.splice(index, 1); + } + + /** + * ** Toggle Toast expand details (expand/collapse). + */ + toggleToastExpandDetails(index: number): void { + this.toasts[index].expanded = !this.toasts[index].expanded; + } + + /** + * ** Copy to clipboard provided object. + */ + copyToClipboard(copy: ToastInternal): void { + try { + this.clipboardService.copy(JSON.stringify(copy)); + } catch (e) { + console.error(e); + + this._handleCopyActionError(); } - - /** - * ** Returns Toast timeout in unit seconds. - */ - getTimeout(toast: ToastInternal): number { - return this._isTypeError(toast) ? 30 : 5; - } - - /** - * ** Returns text for Btn CopyToClipboard. - */ - getCopyToClipboardBtnText(toast: ToastInternal): string { - return this._isTypeError(toast) ? 'Copy to clipboard' : ''; - } - - /** - * ** Returns text for Btn Expand/Collapse. - */ - getExpandBtnText(toast: ToastInternal, index: number): string { - if (this._isTypeError(toast)) { - return this.isToastExpanded(index) ? 'less' : 'more'; - } - - return ''; - } - - /** - * @inheritDoc - */ - ngOnInit() { - this.subscriptions.push( - this.toastService.getNotifications().subscribe((toast: Toast) => { - this.toasts.push({ - ...toast, - id: ToastsComponent.generateID(), - time: ToastsComponent.getDateTimeNow() - }); - }), - this.clipboardService.copyResponse$.subscribe((result) => { - if (!result.isSuccess) { - this._handleCopyActionError(); - } - }) - ); + } + + /** + * ** Returns Toast timeout in unit seconds. + */ + getTimeout(toast: ToastInternal): number { + return this._isTypeError(toast) ? 30 : 5; + } + + /** + * ** Returns text for Btn CopyToClipboard. + */ + getCopyToClipboardBtnText(toast: ToastInternal): string { + return this._isTypeError(toast) ? "Copy to clipboard" : ""; + } + + /** + * ** Returns text for Btn Expand/Collapse. + */ + getExpandBtnText(toast: ToastInternal, index: number): string { + if (this._isTypeError(toast)) { + return this.isToastExpanded(index) ? "less" : "more"; } - private _isTypeError(toast: Toast): boolean { - return toast.type === VmwToastType.FAILURE && !!toast.error; - } + return ""; + } - private _handleCopyActionError(): void { + /** + * @inheritDoc + */ + ngOnInit() { + this.subscriptions.push( + this.toastService.getNotifications().subscribe((toast: Toast) => { this.toasts.push({ - type: VmwToastType.FAILURE, - title: `Copy to clipboard`, - description: `The view definition failed to copy to the clipboard`, - id: ToastsComponent.generateID(), - time: ToastsComponent.getDateTimeNow() + ...toast, + id: ToastsComponent.generateID(), + time: ToastsComponent.getDateTimeNow(), }); - } - - /* eslint-disable @typescript-eslint/member-ordering */ - private static generateID(): number { - return this.toastMessageCounter++; - } - - private static getDateTimeNow(): Date { - return new Date(); - } + }), + this.clipboardService.copyResponse$.subscribe((result) => { + if (!result.isSuccess) { + this._handleCopyActionError(); + } + }), + ); + } + + private _isTypeError(toast: Toast): boolean { + return toast.type === VmwToastType.FAILURE && !!toast.error; + } + + private _handleCopyActionError(): void { + this.toasts.push({ + type: VmwToastType.FAILURE, + title: `Copy to clipboard`, + description: `The view definition failed to copy to the clipboard`, + id: ToastsComponent.generateID(), + time: ToastsComponent.getDateTimeNow(), + }); + } + + /* eslint-disable @typescript-eslint/member-ordering */ + private static generateID(): number { + return this.toastMessageCounter++; + } + + private static getDateTimeNow(): Date { + return new Date(); + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/index.ts index 7526148639..49e513bf96 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { UrlOpenerModel, UrlOpenerTarget } from './model'; -export { UrlOpenerService } from './services'; +export { UrlOpenerModel, UrlOpenerTarget } from "./model"; +export { UrlOpenerService } from "./services"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/index.ts index 731fd17f88..de8c977eaa 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './url-opener.model'; +export * from "./url-opener.model"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/url-opener.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/url-opener.model.ts index 4c70043fbe..b14b3f7bc6 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/url-opener.model.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/model/url-opener.model.ts @@ -3,21 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ConfirmationInputModel } from '../../confirmation'; +import { ConfirmationInputModel } from "../../confirmation"; /** * @inheritDoc * Extended model on top of {@link ConfirmationInputModel} for the needs of Url Opener Service. */ export interface UrlOpenerModel extends ConfirmationInputModel { - /** - * ** Ask for explicit confirmation even when provided Url is internal. - * - * - If not provided default value is false and won't ask for internal url navigation confirmation. - * - Internal url are all urls that don't start with http:// or https:// - * or if they start their Origin is same like Application Origin. - */ - explicitConfirmation?: boolean; + /** + * ** Ask for explicit confirmation even when provided Url is internal. + * + * - If not provided default value is false and won't ask for internal url navigation confirmation. + * - Internal url are all urls that don't start with http:// or https:// + * or if they start their Origin is same like Application Origin. + */ + explicitConfirmation?: boolean; } /** @@ -26,4 +26,4 @@ export interface UrlOpenerModel extends ConfirmationInputModel { * - It would be same Browser tab if value is _self * - It would be new Browser tab if value is _blank */ -export type UrlOpenerTarget = '_self' | '_blank'; +export type UrlOpenerTarget = "_self" | "_blank"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/public-api.ts index 8bedba6307..7aab7b5ad9 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/public-api.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/public-api.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { UrlOpenerModel, UrlOpenerTarget } from './model'; -export { UrlOpenerService } from './services'; -export { UrlOpenerModule } from './url-opener.module'; +export { UrlOpenerModel, UrlOpenerTarget } from "./model"; +export { UrlOpenerService } from "./services"; +export { UrlOpenerModule } from "./url-opener.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/index.ts index a1f48a0e87..9f0d350c1b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './url-opener.service'; +export * from "./url-opener.service"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.spec.ts index d57b59a831..95002c955c 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.spec.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.spec.ts @@ -5,511 +5,609 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { Component, Input } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { Component, Input } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; -import { CookieService } from 'ngx-cookie-service'; +import { CookieService } from "ngx-cookie-service"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { NavigationService } from '../../../core'; +import { NavigationService } from "../../../core"; -import { CallFake } from '../../../unit-testing'; +import { CallFake } from "../../../unit-testing"; -import { ConfirmationService } from '../../confirmation'; +import { ConfirmationService } from "../../confirmation"; -import { UrlOpenerModel } from '../model'; +import { UrlOpenerModel } from "../model"; -import { UrlOpenerService } from './url-opener.service'; +import { UrlOpenerService } from "./url-opener.service"; -describe('UrlOpenerService', () => { - let navigationServiceStub: jasmine.SpyObj; - let confirmationServiceStub: jasmine.SpyObj; - let cookieServiceStub: jasmine.SpyObj; +describe("UrlOpenerService", () => { + let navigationServiceStub: jasmine.SpyObj; + let confirmationServiceStub: jasmine.SpyObj; + let cookieServiceStub: jasmine.SpyObj; - let service: UrlOpenerService; + let service: UrlOpenerService; - beforeEach(() => { - navigationServiceStub = jasmine.createSpyObj('navigationServiceStub', ['navigate']); - confirmationServiceStub = jasmine.createSpyObj('confirmationServiceStub', ['confirm']); - cookieServiceStub = jasmine.createSpyObj('cookieServiceStub', ['get', 'set']); + beforeEach(() => { + navigationServiceStub = jasmine.createSpyObj( + "navigationServiceStub", + ["navigate"], + ); + confirmationServiceStub = jasmine.createSpyObj( + "confirmationServiceStub", + ["confirm"], + ); + cookieServiceStub = jasmine.createSpyObj( + "cookieServiceStub", + ["get", "set"], + ); - TestBed.configureTestingModule({ - declarations: [DummyMessageComponent], - providers: [ - { provide: NavigationService, useValue: navigationServiceStub }, - { provide: ConfirmationService, useValue: confirmationServiceStub }, - { provide: CookieService, useValue: cookieServiceStub }, - UrlOpenerService - ] - }); + TestBed.configureTestingModule({ + declarations: [DummyMessageComponent], + providers: [ + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: ConfirmationService, useValue: confirmationServiceStub }, + { provide: CookieService, useValue: cookieServiceStub }, + UrlOpenerService, + ], + }); - service = TestBed.inject(UrlOpenerService); + service = TestBed.inject(UrlOpenerService); + }); + + it("should verify instance is created", () => { + // Then + expect(service).toBeDefined(); + expect(service).toBeInstanceOf(UrlOpenerService); + expect(service).toBeInstanceOf(TaurusObject); + }); + + describe("Statics::", () => { + describe("Properties::", () => { + describe("|CLASS_NAME|", () => { + it("should verify the value", () => { + // Then + expect(UrlOpenerService.CLASS_NAME).toEqual("UrlOpenerService"); + }); + }); }); + }); - it('should verify instance is created', () => { + describe("Properties::", () => { + describe("|objectUUID|", () => { + it("should verify value is UrlOpenerService", () => { // Then - expect(service).toBeDefined(); - expect(service).toBeInstanceOf(UrlOpenerService); - expect(service).toBeInstanceOf(TaurusObject); + expect(/^UrlOpenerService_/.test(service.objectUUID)).toBeTrue(); + }); + }); + }); + + describe("Methods::", () => { + describe("|initialize|", () => { + const parameters = [ + [null, null], + [ + "аусеров random 72636åßß∂ƒ©˚∆˙˙", + `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be decoded from base64`, + ], + [ + btoa('{"name":"auserov'), + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot parse provided JSON`, + ], + [btoa('{"name":"auserov"}'), null], + ]; + + for (const [cookieValue, consoleErrorValue] of parameters) { + it(`should verify invoking won't throw error when cookie service return -> ${cookieValue}`, () => { + // Given + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + cookieServiceStub.get.and.returnValue(cookieValue); + + // When/Then + expect(() => service.initialize()).not.toThrowError(); + if (consoleErrorValue) { + expect(consoleErrorSpy).toHaveBeenCalledWith(consoleErrorValue); + } + }); + } }); - describe('Statics::', () => { - describe('Properties::', () => { - describe('|CLASS_NAME|', () => { - it('should verify the value', () => { - // Then - expect(UrlOpenerService.CLASS_NAME).toEqual('UrlOpenerService'); - }); + describe("|open|", () => { + describe("Internal_Url::", () => { + it("should verify will open only url", (done) => { + // Given + const url = "http://localhost:9876/pathname/context/10"; + + // When + service.open(url).then((value) => { + expect(value).toBeTrue(); + done(); + }); + }); + + it("should verify will open url and target", (done) => { + // Given + const url = + "http://localhost:9876/pathname/context/12?param1=value10¶m2=value20"; + navigationServiceStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.open(url, "_self").then((value) => { + expect(value).toBeTrue(); + expect(navigationServiceStub.navigate).toHaveBeenCalledWith( + "http://localhost:9876/pathname/context/12", + { + queryParams: { + param1: "value10", + param2: "value20", + }, + }, + ); + done(); + }); + }); + + it(`should verify will open url and target _self and won't throw error when query extraction fails`, (done) => { + // Given + const url = + "http://localhost:9876/pathname/context/14?param1=value10¶m2=value20"; + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + spyOn(URLSearchParams.prototype, "forEach").and.throwError( + new Error("error"), + ); + navigationServiceStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.open(url, "_self").then((value) => { + expect(value).toBeTrue(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, Exception thrown while extracting query string for internal navigation`, + ); + expect(navigationServiceStub.navigate).toHaveBeenCalledWith( + "http://localhost:9876/pathname/context/14", + { + queryParams: {}, + }, + ); + done(); + }); + }); + + it(`should verify will open url and target _blank and will leverage persisted data`, (done) => { + // Given + const url = "http://localhost:9876/pathname/context/16"; + cookieServiceStub.get.and.returnValue( + btoa( + JSON.stringify({ + "http://localhost:9876/pathname/context/16": "1", + }), + ), + ); + service.initialize(); + + // When + service.open(url, "_blank").then((value) => { + expect(value).toBeTrue(); + done(); + }); + }); + + it("should verify will open url, target _blank and confirmation model", (done) => { + // Given + const url = "http://localhost:9876/pathname/context/18"; + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service + .open(url, "_blank", { + explicitConfirmation: true, + } as UrlOpenerModel) + .then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + explicitConfirmation: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(cookieServiceStub.set).toHaveBeenCalledWith( + service["_cookieKey"], + btoa( + JSON.stringify({ + "http://localhost:9876/pathname/context/18": "1", + }), + ), + ); + done(); }); }); - }); - describe('Properties::', () => { - describe('|objectUUID|', () => { - it('should verify value is UrlOpenerService', () => { - // Then - expect(/^UrlOpenerService_/.test(service.objectUUID)).toBeTrue(); + it(`should verify will open url, target _blank and confirmation model and won't persist opt-out because cannot be encoded base64`, (done) => { + // Given + const url = "http://localhost:9876/pathname/context/20/аусеров"; + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service + .open(url, "_blank", { + explicitConfirmation: true, + } as UrlOpenerModel) + .then((value) => { + expect(value).toBeTrue(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be encoded to base64`, + ); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); }); }); - }); - describe('Methods::', () => { - describe('|initialize|', () => { - const parameters = [ - [null, null], - [ - 'аусеров random 72636åßß∂ƒ©˚∆˙˙', - `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be decoded from base64` - ], - [btoa('{"name":"auserov'), `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot parse provided JSON`], - [btoa('{"name":"auserov"}'), null] - ]; - - for (const [cookieValue, consoleErrorValue] of parameters) { - it(`should verify invoking won't throw error when cookie service return -> ${cookieValue}`, () => { - // Given - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - cookieServiceStub.get.and.returnValue(cookieValue); - - // When/Then - expect(() => service.initialize()).not.toThrowError(); - if (consoleErrorValue) { - expect(consoleErrorSpy).toHaveBeenCalledWith(consoleErrorValue); - } - }); - } + it(`should verify will open url, target _blank and confirmation model and won't persist opt-out because cannot be JSON stringify`, (done) => { + // Given + const url = "http://localhost:9876/pathname/context/22"; + spyOn(JSON, "stringify").and.throwError(new Error("error")); + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service + .open(url, "_blank", { + explicitConfirmation: true, + } as UrlOpenerModel) + .then((value) => { + expect(value).toBeTrue(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot serialize provided object`, + ); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); + }); + }); + + it(`should verify will reject if cannot open external url`, (done) => { + // Given + const url = "http://localhost:9876/pathname/context/24"; + // @ts-ignore + spyOn(UrlOpenerService, "_openExternalUrl").and.returnValue(false); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: false }), + ); + + // When + service + .open(url, "_blank", { + explicitConfirmation: true, + message: "some message", + messageComponent: DummyMessageComponent, + messageCode: "some_code_100", + confirmBtnModel: { + text: null, + }, + } as UrlOpenerModel) + .then(() => { + done.fail(`should not enter here`); + }) + .catch((reason) => { + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + message: "some message", + messageComponent: DummyMessageComponent, + messageCode: "some_code_100", + explicitConfirmation: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(reason).toEqual( + new Error( + `${UrlOpenerService.CLASS_NAME}: Exception thrown, cannot open Super Collider url in a new tab`, + ), + ); + done(); + }); + }); + }); + + describe("External_Url::", () => { + it("should verify will open confirmation by default and after will open url", (done) => { + // Given + const url = "http://unknown.something.auserov/pathname/context/10"; + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: false }), + ); + + // When + service.open(url).then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + closable: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); + }); + }); + + it("should verify will open confirmation by default and after will open url and target", (done) => { + // Given + const url = "http://unknown.something.auserov/pathname/context/12"; + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: false }), + ); + + // When + service.open(url, "_blank").then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + closable: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); + }); + }); + + it(`should verify won't open confirmation and will open url and target, leveraging persisted data`, (done) => { + // Given + const url = "http://unknown.something.auserov/pathname/context/14"; + cookieServiceStub.get.and.returnValue( + btoa(JSON.stringify({ "http://unknown.something.auserov": "1" })), + ); + service.initialize(); + + // When + service.open(url, "_blank").then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).not.toHaveBeenCalled(); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); + }); + }); + + it(`should verify will open confirmation and after will open url, for malformed url and won't persist opt-out data`, (done) => { + // Given + const url = "http::://unknown.something.auserov/pathname/context/14"; + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service.open(url).then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + closable: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot extract origin from url`, + ); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); + }); }); - describe('|open|', () => { - describe('Internal_Url::', () => { - it('should verify will open only url', (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/10'; - - // When - service.open(url).then((value) => { - expect(value).toBeTrue(); - done(); - }); - }); - - it('should verify will open url and target', (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/12?param1=value10¶m2=value20'; - navigationServiceStub.navigate.and.returnValue(Promise.resolve(true)); - - // When - service.open(url, '_self').then((value) => { - expect(value).toBeTrue(); - expect(navigationServiceStub.navigate).toHaveBeenCalledWith('http://localhost:9876/pathname/context/12', { - queryParams: { - param1: 'value10', - param2: 'value20' - } - }); - done(); - }); - }); - - it(`should verify will open url and target _self and won't throw error when query extraction fails`, (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/14?param1=value10¶m2=value20'; - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - spyOn(URLSearchParams.prototype, 'forEach').and.throwError(new Error('error')); - navigationServiceStub.navigate.and.returnValue(Promise.resolve(true)); - - // When - service.open(url, '_self').then((value) => { - expect(value).toBeTrue(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, Exception thrown while extracting query string for internal navigation` - ); - expect(navigationServiceStub.navigate).toHaveBeenCalledWith('http://localhost:9876/pathname/context/14', { - queryParams: {} - }); - done(); - }); - }); - - it(`should verify will open url and target _blank and will leverage persisted data`, (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/16'; - cookieServiceStub.get.and.returnValue(btoa(JSON.stringify({ 'http://localhost:9876/pathname/context/16': '1' }))); - service.initialize(); - - // When - service.open(url, '_blank').then((value) => { - expect(value).toBeTrue(); - done(); - }); - }); - - it('should verify will open url, target _blank and confirmation model', (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/18'; - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service.open(url, '_blank', { explicitConfirmation: true } as UrlOpenerModel).then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - explicitConfirmation: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(cookieServiceStub.set).toHaveBeenCalledWith( - service['_cookieKey'], - btoa(JSON.stringify({ 'http://localhost:9876/pathname/context/18': '1' })) - ); - done(); - }); - }); - - it(`should verify will open url, target _blank and confirmation model and won't persist opt-out because cannot be encoded base64`, (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/20/аусеров'; - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service.open(url, '_blank', { explicitConfirmation: true } as UrlOpenerModel).then((value) => { - expect(value).toBeTrue(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be encoded to base64` - ); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify will open url, target _blank and confirmation model and won't persist opt-out because cannot be JSON stringify`, (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/22'; - spyOn(JSON, 'stringify').and.throwError(new Error('error')); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service.open(url, '_blank', { explicitConfirmation: true } as UrlOpenerModel).then((value) => { - expect(value).toBeTrue(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot serialize provided object` - ); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify will reject if cannot open external url`, (done) => { - // Given - const url = 'http://localhost:9876/pathname/context/24'; - // @ts-ignore - spyOn(UrlOpenerService, '_openExternalUrl').and.returnValue(false); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: false })); - - // When - service - .open(url, '_blank', { - explicitConfirmation: true, - message: 'some message', - messageComponent: DummyMessageComponent, - messageCode: 'some_code_100', - confirmBtnModel: { - text: null - } - } as UrlOpenerModel) - .then(() => { - done.fail(`should not enter here`); - }) - .catch((reason) => { - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - message: 'some message', - messageComponent: DummyMessageComponent, - messageCode: 'some_code_100', - explicitConfirmation: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(reason).toEqual( - new Error(`${UrlOpenerService.CLASS_NAME}: Exception thrown, cannot open Super Collider url in a new tab`) - ); - done(); - }); - }); + it(`should verify will open confirmation and after will open url, and will persist opt-out data for non-english origin`, (done) => { + // Given + const url = "http://unknown.something.аусеров/pathname/context/18"; + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service.open(url).then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + closable: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(cookieServiceStub.set).toHaveBeenCalledWith( + service["_cookieKey"], + btoa( + JSON.stringify({ + "http://unknown.something.xn--80aei0bjen": "1", + }), + ), + ); + done(); + }); + }); + + it(`should verify will open confirmation and after will open url, and will persist opt-out data for english origin`, (done) => { + // Given + const url = "http://unknown.something.auserov/pathname/context/30"; + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service.open(url).then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + closable: true, + confirmBtnModel: { + text: "Proceed", + }, + } as UrlOpenerModel); + expect(cookieServiceStub.set).toHaveBeenCalledWith( + service["_cookieKey"], + btoa(JSON.stringify({ "http://unknown.something.auserov": "1" })), + ); + done(); + }); + }); + + it(`should verify will open confirmation and after will open url, target and model, and won't persist opt-out data because cannot be encoded base64`, (done) => { + // Given + const url = "http://unknown.something.аусеров/pathname/context/20"; + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + spyOnProperty(URL.prototype, "origin", "get").and.returnValue( + "аусеров", + ); + + // When + service + .open(url, "_blank", { + title: `Proceed to ${url}`, + message: "Random message", + closable: false, + confirmBtnModel: { + text: null, + iconShape: "window-close", + iconPosition: "right", + }, + }) + .then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url}`, + message: "Random message", + closable: false, + confirmBtnModel: { + text: "Proceed", + iconShape: "window-close", + iconPosition: "right", + }, + } as UrlOpenerModel); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be encoded to base64`, + ); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); }); + }); + + it(`should verify will open confirmation and after will open url, target and model, and won't persist opt-out because cannot be JSON stringify`, (done) => { + // Given + const url = "http://unknown.something.аусеров/pathname/context/22"; + spyOn(JSON, "stringify").and.throwError(new Error("error")); + const consoleErrorSpy = spyOn(console, "error").and.callFake( + CallFake, + ); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service + .open(url, "_blank", { + title: `Proceed to ${url} 2`, + messageComponent: DummyMessageComponent, + closable: false, + confirmBtnModel: { + text: "Proceed 10", + iconShape: "close", + iconPosition: "left", + }, + }) + .then((value) => { + expect(value).toBeTrue(); + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: `Proceed to ${url} 2`, + messageComponent: DummyMessageComponent, + closable: false, + confirmBtnModel: { + text: "Proceed 10", + iconShape: "close", + iconPosition: "left", + }, + } as UrlOpenerModel); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot serialize provided object`, + ); + expect(cookieServiceStub.set).not.toHaveBeenCalled(); + done(); + }); + }); - describe('External_Url::', () => { - it('should verify will open confirmation by default and after will open url', (done) => { - // Given - const url = 'http://unknown.something.auserov/pathname/context/10'; - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: false })); - - // When - service.open(url).then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - closable: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it('should verify will open confirmation by default and after will open url and target', (done) => { - // Given - const url = 'http://unknown.something.auserov/pathname/context/12'; - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: false })); - - // When - service.open(url, '_blank').then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - closable: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify won't open confirmation and will open url and target, leveraging persisted data`, (done) => { - // Given - const url = 'http://unknown.something.auserov/pathname/context/14'; - cookieServiceStub.get.and.returnValue(btoa(JSON.stringify({ 'http://unknown.something.auserov': '1' }))); - service.initialize(); - - // When - service.open(url, '_blank').then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).not.toHaveBeenCalled(); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify will open confirmation and after will open url, for malformed url and won't persist opt-out data`, (done) => { - // Given - const url = 'http::://unknown.something.auserov/pathname/context/14'; - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service.open(url).then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - closable: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot extract origin from url` - ); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify will open confirmation and after will open url, and will persist opt-out data for non-english origin`, (done) => { - // Given - const url = 'http://unknown.something.аусеров/pathname/context/18'; - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service.open(url).then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - closable: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - expect(cookieServiceStub.set).toHaveBeenCalledWith( - service['_cookieKey'], - btoa(JSON.stringify({ 'http://unknown.something.xn--80aei0bjen': '1' })) - ); - done(); - }); - }); - - it(`should verify will open confirmation and after will open url, and will persist opt-out data for english origin`, (done) => { - // Given - const url = 'http://unknown.something.auserov/pathname/context/30'; - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service.open(url).then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - closable: true, - confirmBtnModel: { - text: 'Proceed' - } - } as UrlOpenerModel); - expect(cookieServiceStub.set).toHaveBeenCalledWith( - service['_cookieKey'], - btoa(JSON.stringify({ 'http://unknown.something.auserov': '1' })) - ); - done(); - }); - }); - - it(`should verify will open confirmation and after will open url, target and model, and won't persist opt-out data because cannot be encoded base64`, (done) => { - // Given - const url = 'http://unknown.something.аусеров/pathname/context/20'; - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - spyOnProperty(URL.prototype, 'origin', 'get').and.returnValue('аусеров'); - - // When - service - .open(url, '_blank', { - title: `Proceed to ${url}`, - message: 'Random message', - closable: false, - confirmBtnModel: { - text: null, - iconShape: 'window-close', - iconPosition: 'right' - } - }) - .then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url}`, - message: 'Random message', - closable: false, - confirmBtnModel: { - text: 'Proceed', - iconShape: 'window-close', - iconPosition: 'right' - } - } as UrlOpenerModel); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be encoded to base64` - ); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify will open confirmation and after will open url, target and model, and won't persist opt-out because cannot be JSON stringify`, (done) => { - // Given - const url = 'http://unknown.something.аусеров/pathname/context/22'; - spyOn(JSON, 'stringify').and.throwError(new Error('error')); - const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service - .open(url, '_blank', { - title: `Proceed to ${url} 2`, - messageComponent: DummyMessageComponent, - closable: false, - confirmBtnModel: { - text: 'Proceed 10', - iconShape: 'close', - iconPosition: 'left' - } - }) - .then((value) => { - expect(value).toBeTrue(); - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: `Proceed to ${url} 2`, - messageComponent: DummyMessageComponent, - closable: false, - confirmBtnModel: { - text: 'Proceed 10', - iconShape: 'close', - iconPosition: 'left' - } - } as UrlOpenerModel); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot serialize provided object` - ); - expect(cookieServiceStub.set).not.toHaveBeenCalled(); - done(); - }); - }); - - it(`should verify will reject if cannot open external url`, (done) => { - // Given - const url = 'http://unknown.something.аусеров/pathname/context/100'; - // @ts-ignore - spyOn(UrlOpenerService, '_openExternalUrl').and.returnValue(false); - confirmationServiceStub.confirm.and.returnValue(Promise.resolve({ doNotShowFutureConfirmation: true })); - - // When - service - .open(url, '_blank', { - title: 'Proceed title 100', - message: 'Proceed message 100', - confirmBtnModel: { - text: 'Some text button proceed' - } - }) - .then(() => { - done.fail(`should not enter here`); - }) - .catch((reason) => { - expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ - title: 'Proceed title 100', - message: 'Proceed message 100', - closable: true, - confirmBtnModel: { - text: 'Some text button proceed' - } - } as UrlOpenerModel); - expect(reason).toEqual(new Error(`${UrlOpenerService.CLASS_NAME}: Exception thrown cannot open external url`)); - done(); - }); - }); + it(`should verify will reject if cannot open external url`, (done) => { + // Given + const url = "http://unknown.something.аусеров/pathname/context/100"; + // @ts-ignore + spyOn(UrlOpenerService, "_openExternalUrl").and.returnValue(false); + confirmationServiceStub.confirm.and.returnValue( + Promise.resolve({ doNotShowFutureConfirmation: true }), + ); + + // When + service + .open(url, "_blank", { + title: "Proceed title 100", + message: "Proceed message 100", + confirmBtnModel: { + text: "Some text button proceed", + }, + }) + .then(() => { + done.fail(`should not enter here`); + }) + .catch((reason) => { + expect(confirmationServiceStub.confirm).toHaveBeenCalledWith({ + title: "Proceed title 100", + message: "Proceed message 100", + closable: true, + confirmBtnModel: { + text: "Some text button proceed", + }, + } as UrlOpenerModel); + expect(reason).toEqual( + new Error( + `${UrlOpenerService.CLASS_NAME}: Exception thrown cannot open external url`, + ), + ); + done(); }); }); + }); }); + }); }); @Component({ - selector: 'shared-dummy-message-component', - template: `

    some text

    ` + selector: "shared-dummy-message-component", + template: `

    some text

    `, }) class DummyMessageComponent { - @Input() messageCode: string; + @Input() messageCode: string; } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.ts index c2cc315867..d66cce63c4 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/services/url-opener.service.ts @@ -3,39 +3,42 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Injectable } from '@angular/core'; -import { Params } from '@angular/router'; +import { Injectable } from "@angular/core"; +import { Params } from "@angular/router"; -import { CookieService } from 'ngx-cookie-service'; +import { CookieService } from "ngx-cookie-service"; -import { CollectionsUtil } from '../../../utils'; +import { CollectionsUtil } from "../../../utils"; -import { TaurusObject } from '../../../common'; +import { TaurusObject } from "../../../common"; -import { NavigationService } from '../../../core'; +import { NavigationService } from "../../../core"; -import { ConfirmationOutputModel, ConfirmationService } from '../../confirmation'; +import { + ConfirmationOutputModel, + ConfirmationService, +} from "../../confirmation"; -import { UrlOpenerModel, UrlOpenerTarget } from '../model'; +import { UrlOpenerModel, UrlOpenerTarget } from "../model"; /** * ** Internal service model. */ interface NextStepModel { - /** - * ** Is provided url external. - */ - external: boolean; - /** - * ** Sanitized url. - * - * - Could be use in future if some sanitization is needed for some special cases. - */ - sanitizedUrl: string; - /** - * ** Does url be open in new tab. - */ - newTab: boolean; + /** + * ** Is provided url external. + */ + external: boolean; + /** + * ** Sanitized url. + * + * - Could be use in future if some sanitization is needed for some special cases. + */ + sanitizedUrl: string; + /** + * ** Does url be open in new tab. + */ + newTab: boolean; } /** @@ -52,389 +55,449 @@ interface NextStepModel { */ @Injectable() export class UrlOpenerService extends TaurusObject { - /** - * @inheritDoc - */ - static override readonly CLASS_NAME: string = 'UrlOpenerService'; - - /** - * ** Application origin, resolved upon service declaration. - * - * @private - */ - private readonly _origin: string; - - /** - * ** Cookie key where service state is persisted, resolved upon service declaration. - * - * - It's service Class Name encoded to Base64. - * - * @private - */ - private readonly _cookieKey: string; - - /** - * ** Application state, for which origins/urls confirmation should be skipped. - * - * @private - */ - private _skippedOriginsUrlsMap: Record = {}; - - /** - * ** Constructor. - */ - constructor( - private readonly navigationService: NavigationService, - private readonly confirmationService: ConfirmationService, - private readonly cookieService: CookieService - ) { - super(UrlOpenerService.CLASS_NAME); - - try { - this._origin = window.location.origin; - this._cookieKey = UrlOpenerService._encodeBase64(UrlOpenerService.CLASS_NAME); - } catch (e) { - console.error(`${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot identify Cookie key and Origin`); - } + /** + * @inheritDoc + */ + static override readonly CLASS_NAME: string = "UrlOpenerService"; + + /** + * ** Application origin, resolved upon service declaration. + * + * @private + */ + private readonly _origin: string; + + /** + * ** Cookie key where service state is persisted, resolved upon service declaration. + * + * - It's service Class Name encoded to Base64. + * + * @private + */ + private readonly _cookieKey: string; + + /** + * ** Application state, for which origins/urls confirmation should be skipped. + * + * @private + */ + private _skippedOriginsUrlsMap: Record = {}; + + /** + * ** Constructor. + */ + constructor( + private readonly navigationService: NavigationService, + private readonly confirmationService: ConfirmationService, + private readonly cookieService: CookieService, + ) { + super(UrlOpenerService.CLASS_NAME); + + try { + this._origin = window.location.origin; + this._cookieKey = UrlOpenerService._encodeBase64( + UrlOpenerService.CLASS_NAME, + ); + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot identify Cookie key and Origin`, + ); } - - /** - * ** Open provided Url to default _blank target. - * - * - Internal urls are open directly without confirmation, while external are always prompt for confirmation. - * - Returned Promise is resolved upon navigation ends with true (successful navigation) or false (unsuccessful navigation), - * or if there is some internal handled error it will be rejected with instance of Error and message of the specific problem. - * - Every url which starts with pattern http:// or https:// and its origin is different from Application origin is marked as external url. - * - Everything else is internal url. - */ - open(url: string): Promise; - /** - * ** Open provided Url using provided target _self or _blank - * - * - Internal urls are open directly without confirmation, while external are always prompt for confirmation. - * - Returned Promise is resolved upon navigation ends with true (successful navigation) or false (unsuccessful navigation), - * or if there is some internal handled error it will be rejected with instance of Error and message of the specific problem. - * - Every url which starts with pattern http:// or https:// and its origin is different from Application origin is marked as external url. - * - Everything else is internal url. - */ - open(url: string, target: UrlOpenerTarget): Promise; - /** - * ** Open provided Url using provided target _self or _blank and utilizing provided model and service will set some defaults for optional fields. - * - * - Internal urls could be open directly without confirmation or explicitly with confirmation depend on provided model, - * while external are always prompt for confirmation it there is no option to skip confirmation for next navigations, - * and User prompt such confirmation to be skipped. - * - Skipped confirmation are persisted in Cookie storage as Origin for external urls and as Url for internal urls. - * - Returned Promise is resolved upon navigation ends with true (successful navigation) or false (unsuccessful navigation), - * or rejected with value string if it's on User behalf when confirmation is closable or with button cancel, - * or if there is some internal handled error it will be rejected with instance of Error and message of the specific problem. - */ - open(url: string, target: UrlOpenerTarget, model: UrlOpenerModel): Promise; - /** - * @inheritDoc - */ - open(url: string, target: UrlOpenerTarget = '_blank', model: UrlOpenerModel = null): Promise { - const _model: UrlOpenerModel = { ...(model ?? {}) } as UrlOpenerModel; - const _nextStep = this._resolveNextStep(url, target); - - if (_nextStep.external) { - return this._executeExternalNavigation(url, _model, _nextStep); - } - - return this._executeInternalNavigation(url, _model, _nextStep); + } + + /** + * ** Open provided Url to default _blank target. + * + * - Internal urls are open directly without confirmation, while external are always prompt for confirmation. + * - Returned Promise is resolved upon navigation ends with true (successful navigation) or false (unsuccessful navigation), + * or if there is some internal handled error it will be rejected with instance of Error and message of the specific problem. + * - Every url which starts with pattern http:// or https:// and its origin is different from Application origin is marked as external url. + * - Everything else is internal url. + */ + open(url: string): Promise; + /** + * ** Open provided Url using provided target _self or _blank + * + * - Internal urls are open directly without confirmation, while external are always prompt for confirmation. + * - Returned Promise is resolved upon navigation ends with true (successful navigation) or false (unsuccessful navigation), + * or if there is some internal handled error it will be rejected with instance of Error and message of the specific problem. + * - Every url which starts with pattern http:// or https:// and its origin is different from Application origin is marked as external url. + * - Everything else is internal url. + */ + open(url: string, target: UrlOpenerTarget): Promise; + /** + * ** Open provided Url using provided target _self or _blank and utilizing provided model and service will set some defaults for optional fields. + * + * - Internal urls could be open directly without confirmation or explicitly with confirmation depend on provided model, + * while external are always prompt for confirmation it there is no option to skip confirmation for next navigations, + * and User prompt such confirmation to be skipped. + * - Skipped confirmation are persisted in Cookie storage as Origin for external urls and as Url for internal urls. + * - Returned Promise is resolved upon navigation ends with true (successful navigation) or false (unsuccessful navigation), + * or rejected with value string if it's on User behalf when confirmation is closable or with button cancel, + * or if there is some internal handled error it will be rejected with instance of Error and message of the specific problem. + */ + open( + url: string, + target: UrlOpenerTarget, + model: UrlOpenerModel, + ): Promise; + /** + * @inheritDoc + */ + open( + url: string, + target: UrlOpenerTarget = "_blank", + model: UrlOpenerModel = null, + ): Promise { + const _model: UrlOpenerModel = { ...(model ?? {}) } as UrlOpenerModel; + const _nextStep = this._resolveNextStep(url, target); + + if (_nextStep.external) { + return this._executeExternalNavigation(url, _model, _nextStep); } - /** - * ** Initialize service. - * - * - Should be invoked only once. - * - Ideal place for invoking is AppComponent.ngOnInit(). - */ - initialize(): void { - const extractSkippedOriginsMap = this._extractSkippedUrlsMap(); - if (extractSkippedOriginsMap) { - this._skippedOriginsUrlsMap = extractSkippedOriginsMap; - } + return this._executeInternalNavigation(url, _model, _nextStep); + } + + /** + * ** Initialize service. + * + * - Should be invoked only once. + * - Ideal place for invoking is AppComponent.ngOnInit(). + */ + initialize(): void { + const extractSkippedOriginsMap = this._extractSkippedUrlsMap(); + if (extractSkippedOriginsMap) { + this._skippedOriginsUrlsMap = extractSkippedOriginsMap; } + } - private _resolveNextStep(url: string, target: UrlOpenerTarget): NextStepModel { - let newTab = false; - let external = false; - - if (target === '_blank') { - newTab = true; - } - - if (new RegExp(`^${this._origin}`).test(url)) { - external = false; - } else if (/^(http|https):\/?\/?/.test(url)) { - external = true; - } + private _resolveNextStep( + url: string, + target: UrlOpenerTarget, + ): NextStepModel { + let newTab = false; + let external = false; - return { - newTab, - external, - sanitizedUrl: url - }; + if (target === "_blank") { + newTab = true; } - private _executeExternalNavigation(url: string, model: UrlOpenerModel, nextStep: NextStepModel): Promise { - const urlOriginData = this._getExternalUrlOriginData(url); + if (new RegExp(`^${this._origin}`).test(url)) { + external = false; + } else if (/^(http|https):\/?\/?/.test(url)) { + external = true; + } - let confirmationPromise: Promise; + return { + newTab, + external, + sanitizedUrl: url, + }; + } + + private _executeExternalNavigation( + url: string, + model: UrlOpenerModel, + nextStep: NextStepModel, + ): Promise { + const urlOriginData = this._getExternalUrlOriginData(url); + + let confirmationPromise: Promise; + + if (urlOriginData === "1") { + confirmationPromise = Promise.resolve({ + doNotShowFutureConfirmation: false, + }); + } else { + if (!CollectionsUtil.isStringWithContent(model.title)) { + model.title = `Proceed to ${url}`; + } + + if (CollectionsUtil.isNil(model.closable)) { + model.closable = true; + } + + if (CollectionsUtil.isNil(model.confirmBtnModel)) { + model.confirmBtnModel = { + text: "Proceed", + }; + } else if ( + !CollectionsUtil.isStringWithContent(model.confirmBtnModel.text) + ) { + model.confirmBtnModel.text = "Proceed"; + } - if (urlOriginData === '1') { - confirmationPromise = Promise.resolve({ doNotShowFutureConfirmation: false }); - } else { - if (!CollectionsUtil.isStringWithContent(model.title)) { - model.title = `Proceed to ${url}`; - } + confirmationPromise = this.confirmationService.confirm(model); + } - if (CollectionsUtil.isNil(model.closable)) { - model.closable = true; - } + return confirmationPromise.then((data) => { + if (data.doNotShowFutureConfirmation) { + this._persistSkippedExternalUrlOrigin(url); + } + + const isSuccessful = UrlOpenerService._openExternalUrl( + nextStep.sanitizedUrl, + nextStep.newTab, + ); + if (isSuccessful) { + return true; + } + + return Promise.reject( + new Error( + `${UrlOpenerService.CLASS_NAME}: Exception thrown cannot open external url`, + ), + ); + }); + } + + private _executeInternalNavigation( + url: string, + model: UrlOpenerModel, + nextStep: NextStepModel, + ): Promise { + const urlData = this._getInternalUrlData(url); + + let confirmationPromise: Promise; + + if (urlData === "1" || !model.explicitConfirmation) { + confirmationPromise = Promise.resolve({ + doNotShowFutureConfirmation: false, + }); + } else { + if (!CollectionsUtil.isStringWithContent(model.title)) { + model.title = `Proceed to ${url}`; + } + + if (CollectionsUtil.isNil(model.confirmBtnModel)) { + model.confirmBtnModel = { + text: "Proceed", + }; + } else if ( + !CollectionsUtil.isStringWithContent(model.confirmBtnModel.text) + ) { + model.confirmBtnModel.text = "Proceed"; + } - if (CollectionsUtil.isNil(model.confirmBtnModel)) { - model.confirmBtnModel = { - text: 'Proceed' - }; - } else if (!CollectionsUtil.isStringWithContent(model.confirmBtnModel.text)) { - model.confirmBtnModel.text = 'Proceed'; - } + confirmationPromise = this.confirmationService.confirm(model); + } - confirmationPromise = this.confirmationService.confirm(model); + return confirmationPromise.then((data) => { + if (data.doNotShowFutureConfirmation) { + this._persistSkippedInternalUrl(url); + } + + if (nextStep.newTab) { + const isSuccessful = UrlOpenerService._openExternalUrl( + nextStep.sanitizedUrl, + true, + ); + if (isSuccessful) { + return true; } - return confirmationPromise.then((data) => { - if (data.doNotShowFutureConfirmation) { - this._persistSkippedExternalUrlOrigin(url); - } - - const isSuccessful = UrlOpenerService._openExternalUrl(nextStep.sanitizedUrl, nextStep.newTab); - if (isSuccessful) { - return true; - } - - return Promise.reject(new Error(`${UrlOpenerService.CLASS_NAME}: Exception thrown cannot open external url`)); - }); + return Promise.reject( + new Error( + `${UrlOpenerService.CLASS_NAME}: Exception thrown, cannot open Super Collider url in a new tab`, + ), + ); + } + + const _queryParams: Params = {}; + let _sanitizedUrl: string = nextStep.sanitizedUrl; + + try { + const queryStringStartIndex = url.indexOf("?"); + + if (queryStringStartIndex !== -1) { + _sanitizedUrl = url.substring(0, queryStringStartIndex); + const queryString = url.substring(queryStringStartIndex); + new URLSearchParams(queryString).forEach((value, key) => { + _queryParams[key] = value; + }); + } + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, Exception thrown while extracting query string for internal navigation`, + ); + } + + return this.navigationService.navigate(_sanitizedUrl, { + queryParams: _queryParams, + }); + }); + } + + private _getExternalUrlOriginData(url: string): "1" | null { + const urlOrigin = UrlOpenerService._getUrlOrigin(url); + + if (!this._skippedOriginsUrlsMap[urlOrigin]) { + return null; } - private _executeInternalNavigation(url: string, model: UrlOpenerModel, nextStep: NextStepModel): Promise { - const urlData = this._getInternalUrlData(url); + return this._skippedOriginsUrlsMap[urlOrigin]; + } - let confirmationPromise: Promise; + private _persistSkippedExternalUrlOrigin(url: string): void { + const urlOrigin = UrlOpenerService._getUrlOrigin(url); - if (urlData === '1' || !model.explicitConfirmation) { - confirmationPromise = Promise.resolve({ doNotShowFutureConfirmation: false }); - } else { - if (!CollectionsUtil.isStringWithContent(model.title)) { - model.title = `Proceed to ${url}`; - } - - if (CollectionsUtil.isNil(model.confirmBtnModel)) { - model.confirmBtnModel = { - text: 'Proceed' - }; - } else if (!CollectionsUtil.isStringWithContent(model.confirmBtnModel.text)) { - model.confirmBtnModel.text = 'Proceed'; - } + if (!urlOrigin) { + return; + } - confirmationPromise = this.confirmationService.confirm(model); - } + this._persistSkippedInternalUrl(urlOrigin); + } - return confirmationPromise.then((data) => { - if (data.doNotShowFutureConfirmation) { - this._persistSkippedInternalUrl(url); - } - - if (nextStep.newTab) { - const isSuccessful = UrlOpenerService._openExternalUrl(nextStep.sanitizedUrl, true); - if (isSuccessful) { - return true; - } - - return Promise.reject( - new Error(`${UrlOpenerService.CLASS_NAME}: Exception thrown, cannot open Super Collider url in a new tab`) - ); - } - - const _queryParams: Params = {}; - let _sanitizedUrl: string = nextStep.sanitizedUrl; - - try { - const queryStringStartIndex = url.indexOf('?'); - - if (queryStringStartIndex !== -1) { - _sanitizedUrl = url.substring(0, queryStringStartIndex); - const queryString = url.substring(queryStringStartIndex); - new URLSearchParams(queryString).forEach((value, key) => { - _queryParams[key] = value; - }); - } - } catch (e) { - console.error( - `${UrlOpenerService.CLASS_NAME}: Potential bug found, Exception thrown while extracting query string for internal navigation` - ); - } - - return this.navigationService.navigate(_sanitizedUrl, { queryParams: _queryParams }); - }); + private _getInternalUrlData(url: string): "1" | null { + if (!this._skippedOriginsUrlsMap[url]) { + return null; } - private _getExternalUrlOriginData(url: string): '1' | null { - const urlOrigin = UrlOpenerService._getUrlOrigin(url); + return this._skippedOriginsUrlsMap[url]; + } - if (!this._skippedOriginsUrlsMap[urlOrigin]) { - return null; - } - - return this._skippedOriginsUrlsMap[urlOrigin]; + private _persistSkippedInternalUrl(url: string): void { + if (this._skippedOriginsUrlsMap[url]) { + return; } - private _persistSkippedExternalUrlOrigin(url: string): void { - const urlOrigin = UrlOpenerService._getUrlOrigin(url); + this._skippedOriginsUrlsMap[url] = "1"; - if (!urlOrigin) { - return; - } + this._persistSkippedUrlsMap(); + } - this._persistSkippedInternalUrl(urlOrigin); + private _extractSkippedUrlsMap(): Record { + const extractedOriginsMap = this.cookieService.get(this._cookieKey); + if (!extractedOriginsMap) { + return null; } - private _getInternalUrlData(url: string): '1' | null { - if (!this._skippedOriginsUrlsMap[url]) { - return null; - } - - return this._skippedOriginsUrlsMap[url]; + const decodedOriginsMap = + UrlOpenerService._decodeBase64(extractedOriginsMap); + if (!decodedOriginsMap) { + return null; } - private _persistSkippedInternalUrl(url: string): void { - if (this._skippedOriginsUrlsMap[url]) { - return; - } - - this._skippedOriginsUrlsMap[url] = '1'; - - this._persistSkippedUrlsMap(); + const parsedOriginsMap = + UrlOpenerService._parseToJSON>(decodedOriginsMap); + if (!parsedOriginsMap) { + return null; } - private _extractSkippedUrlsMap(): Record { - const extractedOriginsMap = this.cookieService.get(this._cookieKey); - if (!extractedOriginsMap) { - return null; - } + return parsedOriginsMap; + } - const decodedOriginsMap = UrlOpenerService._decodeBase64(extractedOriginsMap); - if (!decodedOriginsMap) { - return null; - } + private _persistSkippedUrlsMap(): void { + if (!this._skippedOriginsUrlsMap) { + return; + } - const parsedOriginsMap = UrlOpenerService._parseToJSON>(decodedOriginsMap); - if (!parsedOriginsMap) { - return null; - } + const serializedOriginsMap = UrlOpenerService._serializeObject( + this._skippedOriginsUrlsMap, + ); + if (!serializedOriginsMap) { + return; + } - return parsedOriginsMap; + const encodedOriginsMap = + UrlOpenerService._encodeBase64(serializedOriginsMap); + if (!encodedOriginsMap) { + return; } - private _persistSkippedUrlsMap(): void { - if (!this._skippedOriginsUrlsMap) { - return; - } + this.cookieService.set(this._cookieKey, encodedOriginsMap); + } - const serializedOriginsMap = UrlOpenerService._serializeObject(this._skippedOriginsUrlsMap); - if (!serializedOriginsMap) { - return; - } + private static _openExternalUrl(url: string, newTab: boolean): boolean { + try { + window.open(url, newTab ? "_blank" : "_self", "noopener"); - const encodedOriginsMap = UrlOpenerService._encodeBase64(serializedOriginsMap); - if (!encodedOriginsMap) { - return; - } + return true; + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Cannot open external url, check your Browser security config if allows opening new tabs`, + ); - this.cookieService.set(this._cookieKey, encodedOriginsMap); + return false; } + } - private static _openExternalUrl(url: string, newTab: boolean): boolean { - try { - window.open(url, newTab ? '_blank' : '_self', 'noopener'); + private static _getUrlOrigin(url: string): string { + try { + const _url = new URL(url); - return true; - } catch (e) { - console.error( - `${UrlOpenerService.CLASS_NAME}: Cannot open external url, check your Browser security config if allows opening new tabs` - ); + return _url.origin; + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot extract origin from url`, + ); - return false; - } + return null; } + } - private static _getUrlOrigin(url: string): string { - try { - const _url = new URL(url); - - return _url.origin; - } catch (e) { - console.error(`${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot extract origin from url`); - - return null; - } + private static _encodeBase64(value: string): string { + if (!value) { + return null; } - private static _encodeBase64(value: string): string { - if (!value) { - return null; - } - - try { - return btoa(value); - } catch (e) { - console.error(`${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be encoded to base64`); + try { + return btoa(value); + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be encoded to base64`, + ); - return null; - } + return null; } + } - private static _decodeBase64(value: string): string { - if (!value) { - return null; - } + private static _decodeBase64(value: string): string { + if (!value) { + return null; + } - try { - return atob(value); - } catch (e) { - console.error(`${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be decoded from base64`); + try { + return atob(value); + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, provided value cannot be decoded from base64`, + ); - return null; - } + return null; } + } - private static _serializeObject(value: unknown): string { - if (!value) { - return null; - } + private static _serializeObject(value: unknown): string { + if (!value) { + return null; + } - try { - return JSON.stringify(value); - } catch (e) { - console.error(`${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot serialize provided object`); + try { + return JSON.stringify(value); + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot serialize provided object`, + ); - return null; - } + return null; } + } - private static _parseToJSON(value: string): T { - if (!value) { - return null; - } + private static _parseToJSON(value: string): T { + if (!value) { + return null; + } - try { - return JSON.parse(value) as T; - } catch (e) { - console.error(`${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot parse provided JSON`); + try { + return JSON.parse(value) as T; + } catch (e) { + console.error( + `${UrlOpenerService.CLASS_NAME}: Potential bug found, cannot parse provided JSON`, + ); - return null; - } + return null; } + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/url-opener.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/url-opener.module.ts index b2a1006b0e..fb8b22f936 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/url-opener.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/url-opener/url-opener.module.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; /** * ** Url Opener module @@ -12,6 +12,6 @@ import { CommonModule } from '@angular/common'; * @author gorankokin */ @NgModule({ - imports: [CommonModule] + imports: [CommonModule], }) export class UrlOpenerModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/vdk-shared-features.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/vdk-shared-features.module.ts index 894c277323..728f99851b 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/vdk-shared-features.module.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/vdk-shared-features.module.ts @@ -3,96 +3,101 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { RouterModule } from "@angular/router"; -import { ClarityModule } from '@clr/angular'; +import { ClarityModule } from "@clr/angular"; -import { SharedFeaturesConfig } from './_model'; +import { SharedFeaturesConfig } from "./_model"; -import { VdkSharedComponentsModule } from '../commons'; +import { VdkSharedComponentsModule } from "../commons"; -import { SHARED_FEATURES_CONFIG_TOKEN } from './_token'; +import { SHARED_FEATURES_CONFIG_TOKEN } from "./_token"; -import { PlaceholderModule } from './placeholder/placeholder.module'; +import { PlaceholderModule } from "./placeholder/placeholder.module"; -import { WarningModule } from './warning/warning.module'; +import { WarningModule } from "./warning/warning.module"; -import { ToastsModule } from './toasts/toasts.module'; -import { ToastService } from './toasts/service'; +import { ToastsModule } from "./toasts/toasts.module"; +import { ToastService } from "./toasts/service"; -import { ErrorHandlerService } from './error-handler/services'; +import { ErrorHandlerService } from "./error-handler/services"; -import { DirectivesModule } from './directives/directives.module'; +import { DirectivesModule } from "./directives/directives.module"; -import { PipesModule } from './pipes/pipes.module'; +import { PipesModule } from "./pipes/pipes.module"; -import { DynamicComponentsModule } from './dynamic-components/dynamic-components.module'; -import { DynamicComponentsService } from './dynamic-components'; +import { DynamicComponentsModule } from "./dynamic-components/dynamic-components.module"; +import { DynamicComponentsService } from "./dynamic-components"; -import { ConfirmationModule } from './confirmation/confirmation.module'; -import { ConfirmationService } from './confirmation'; +import { ConfirmationModule } from "./confirmation/confirmation.module"; +import { ConfirmationService } from "./confirmation"; -import { UrlOpenerModule } from './url-opener/url-opener.module'; -import { UrlOpenerService } from './url-opener'; +import { UrlOpenerModule } from "./url-opener/url-opener.module"; +import { UrlOpenerService } from "./url-opener"; @NgModule({ - imports: [ - CommonModule, - RouterModule, - ClarityModule, - VdkSharedComponentsModule.forChild(), - ConfirmationModule, - DirectivesModule, - DynamicComponentsModule, - PlaceholderModule, - PipesModule, - ToastsModule, - UrlOpenerModule, - WarningModule - ], - exports: [ - ConfirmationModule, - DirectivesModule, - DynamicComponentsModule, - PlaceholderModule, - PipesModule, - ToastsModule, - UrlOpenerModule, - WarningModule - ] + imports: [ + CommonModule, + RouterModule, + ClarityModule, + VdkSharedComponentsModule.forChild(), + ConfirmationModule, + DirectivesModule, + DynamicComponentsModule, + PlaceholderModule, + PipesModule, + ToastsModule, + UrlOpenerModule, + WarningModule, + ], + exports: [ + ConfirmationModule, + DirectivesModule, + DynamicComponentsModule, + PlaceholderModule, + PipesModule, + ToastsModule, + UrlOpenerModule, + WarningModule, + ], }) export class VdkSharedFeaturesModule { - /** - * ** Provides VdkSharedFeaturesModule and all Services related to Shared Module features. - * - * - Should be invoked only once for entire project. - * - Not inside FeatureModule (lazy loaded Module). - * - In other modules import only VdkSharedFeaturesModule or VdkSharedFeaturesModule.forChild(). - */ - static forRoot(featuresConfig?: SharedFeaturesConfig): ModuleWithProviders { - return { - ngModule: VdkSharedFeaturesModule, - providers: [ - { provide: SHARED_FEATURES_CONFIG_TOKEN, useValue: featuresConfig ?? {} }, - ConfirmationService, - DynamicComponentsService, - ErrorHandlerService, - ToastService, - UrlOpenerService - ] - }; - } - - /** - * ** Provides VdkSharedFeaturesModule. - * - * - Should be invoked in FeatureModules (lazy loaded Modules). - */ - static forChild(): ModuleWithProviders { - return { - ngModule: VdkSharedFeaturesModule - }; - } + /** + * ** Provides VdkSharedFeaturesModule and all Services related to Shared Module features. + * + * - Should be invoked only once for entire project. + * - Not inside FeatureModule (lazy loaded Module). + * - In other modules import only VdkSharedFeaturesModule or VdkSharedFeaturesModule.forChild(). + */ + static forRoot( + featuresConfig?: SharedFeaturesConfig, + ): ModuleWithProviders { + return { + ngModule: VdkSharedFeaturesModule, + providers: [ + { + provide: SHARED_FEATURES_CONFIG_TOKEN, + useValue: featuresConfig ?? {}, + }, + ConfirmationService, + DynamicComponentsService, + ErrorHandlerService, + ToastService, + UrlOpenerService, + ], + }; + } + + /** + * ** Provides VdkSharedFeaturesModule. + * + * - Should be invoked in FeatureModules (lazy loaded Modules). + */ + static forChild(): ModuleWithProviders { + return { + ngModule: VdkSharedFeaturesModule, + }; + } } diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/index.ts index b0e6567b0f..dc961d56ba 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './warning'; +export * from "./warning"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/index.ts index 017588a050..8bf16b7431 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/index.ts +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './warning.component'; +export * from "./warning.component"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/warning.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/warning.component.html index d0a3b14ab6..f7a1e9bf45 100644 --- a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/warning.component.html +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/warning/components/warning/warning.component.html @@ -4,83 +4,82 @@ --> - + - - - - + + + + - - - - + + + + -