diff --git a/.github/workflows/deploy-stage.yml b/.github/workflows/deploy-stage.yml index 4e3deea2a..410d9b7ae 100644 --- a/.github/workflows/deploy-stage.yml +++ b/.github/workflows/deploy-stage.yml @@ -10,6 +10,9 @@ env: jobs: deploy: runs-on: ubuntu-latest + concurrency: + group: deploy-console + cancel-in-progress: false container: image: node:18-alpine3.18 steps: diff --git a/.github/workflows/dev-e2e-tests.yml b/.github/workflows/dev-e2e-tests.yml index c7896426f..0ac51ff4a 100644 --- a/.github/workflows/dev-e2e-tests.yml +++ b/.github/workflows/dev-e2e-tests.yml @@ -16,7 +16,7 @@ jobs: matrix: group: [2, 3, 4, 5, 6, 7] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 @@ -47,7 +47,7 @@ jobs: - name: Upload Cypress Screenshots if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cypress_screenshots_${{ matrix.group }} path: cypress/screenshots @@ -55,18 +55,19 @@ jobs: - name: Upload Cypress Videos if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cypress_videos_${{ matrix.group }} path: cypress/videos retention-days: 1 - name: Upload Coverage Files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage_reportx + name: coverage_report_${{ matrix.group }} path: .nyc_output/* retention-days: 1 + include-hidden-files: true download_and_merge: name: Download and Merge Coverage Reports @@ -86,12 +87,14 @@ jobs: run: npm install --global nyc - name: Download Coverage Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: coverage_report + pattern: coverage_report_* + merge-multiple: true - name: Merge Coverage Reports - run: npx nyc merge ./coverage_report/coverage_reportx .nyc_output/out.json + run: npx nyc merge ./coverage_report .nyc_output/out.json - name: Generate Text Coverage Report run: npx nyc report --reporter=text-summary @@ -112,7 +115,7 @@ jobs: - name: Upload Coverage Report Artifact if: ${{ always() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage_e2e_report path: coverage.zip diff --git a/azion.config.cjs b/azion.config.cjs index 16851515e..23179d8e6 100644 --- a/azion.config.cjs +++ b/azion.config.cjs @@ -242,47 +242,68 @@ const backRules = [ }, rewrite: '/billing/graphql' } + }, + { + name: 'Route List Client Ids to Github', + description: 'this route will get the client ids of the accounts released to the console', + match: '^/api/allowed-accounts', + behavior: { + forwardCookies: true, + setOrigin: { + name: 'origin-github-allowed-accounts', + type: 'single_origin' + }, + rewrite: '/aziontech/console-client-list/main/clids.json' + } } ] const AzionConfig = { cache: [...cacheConfig], - origin: addStagePrefix([ - { - name: 'origin-storage-default', - type: 'object_storage' - }, - { - name: 'origin-manager', - type: 'single_origin', - hostHeader: `manager.azion.com`, - addresses: [`manager.azion.com`] - }, - { - name: 'origin-marketplace', - type: 'single_origin', - hostHeader: `marketplace.azion.com`, - addresses: [`marketplace.azion.com`] - }, - { - name: 'origin-cities', - type: 'single_origin', - hostHeader: `cities.azion.com`, - addresses: [`cities.azion.com`] - }, + origin: [ + ...addStagePrefix([ + { + name: 'origin-storage-default', + type: 'object_storage' + }, + { + name: 'origin-manager', + type: 'single_origin', + hostHeader: `manager.azion.com`, + addresses: [`manager.azion.com`] + }, + { + name: 'origin-marketplace', + type: 'single_origin', + hostHeader: `marketplace.azion.com`, + addresses: [`marketplace.azion.com`] + }, + { + name: 'origin-cities', + type: 'single_origin', + hostHeader: `cities.azion.com`, + addresses: [`cities.azion.com`] + }, + { + name: 'origin-sso', + type: 'single_origin', + hostHeader: `sso.azion.com`, + addresses: [`sso.azion.com`] + }, + { + name: 'origin-api', + type: 'single_origin', + hostHeader: `api.azion.com`, + addresses: [`api.azion.com`] + }, + ]), { - name: 'origin-sso', + name: 'origin-github-allowed-accounts', type: 'single_origin', - hostHeader: `sso.azion.com`, - addresses: [`sso.azion.com`] + hostHeader: `raw.githubusercontent.com`, + addresses: [`raw.githubusercontent.com`] }, - { - name: 'origin-api', - type: 'single_origin', - hostHeader: `api.azion.com`, - addresses: [`api.azion.com`] - } - ]), + ], rules: { request: [...commonRules, ...frontRules, ...backRules], response: [ diff --git a/azion/stage/azion.json b/azion/stage/azion.json index 1e318a5cc..47caa2a41 100644 --- a/azion/stage/azion.json +++ b/azion/stage/azion.json @@ -63,6 +63,11 @@ "origin-id": 146195, "origin-key": "c92f57e1-79d6-4bcc-9493-2af4ad9f4dd5", "name": "origin-cities" + }, + { + "origin-id": 153113, + "origin-key": "ab771dc5-8ee9-4f9f-bb5d-3fa6fe9483c5", + "name": "origin-github-allowed-accounts" } ], "rules-engine": { @@ -142,6 +147,11 @@ "id": 277563, "name": "Secure Headers", "phase": "response" + }, + { + "id": 294965, + "name": "Route List Client Ids to Github", + "phase": "request" } ] }, diff --git a/cypress/e2e/edge-firewall/create-edge-firewall-network-list.cy.js b/cypress/e2e/edge-firewall/create-edge-firewall-network-list.cy.js index 894fecf2a..6dbcb65d8 100644 --- a/cypress/e2e/edge-firewall/create-edge-firewall-network-list.cy.js +++ b/cypress/e2e/edge-firewall/create-edge-firewall-network-list.cy.js @@ -51,11 +51,12 @@ describe('Edge Firewall spec', { tags: ['@dev5'] }, () => { cy.get(selectors.edgeFirewall.ruleDescriptionInput).type('My Rule Description') // Act - Set Criteria + cy.intercept('GET', '/api/v3/network_lists?page=1&page_size=200').as('networkList') cy.get(selectors.edgeFirewall.ruleCriteriaVariableDropdown).click() cy.get(selectors.edgeFirewall.ruleCriteriaVariableDropdownNetworkLists).click() cy.get(selectors.edgeFirewall.ruleCriteriaOperatorDropdown).click() cy.get(selectors.edgeFirewall.ruleCriteriaOperatorFirstOption).click() - cy.wait(1000) + cy.wait('@networkList') cy.get(selectors.edgeFirewall.ruleCriteriaNetworkListDropdown).click() cy.get(selectors.edgeFirewall.ruleCriteriaNetworkListFilter).clear() cy.get(selectors.edgeFirewall.ruleCriteriaNetworkListFilter).type(networkListName) @@ -98,4 +99,4 @@ describe('Edge Firewall spec', { tags: ['@dev5'] }, () => { cy.verifyToast('Edge Firewall successfully deleted') }) }) -}) \ No newline at end of file +}) diff --git a/cypress/e2e/edge-firewall/create-edge-firewall-waf.cy.js b/cypress/e2e/edge-firewall/create-edge-firewall-waf.cy.js index 290c488ae..c68dd0788 100644 --- a/cypress/e2e/edge-firewall/create-edge-firewall-waf.cy.js +++ b/cypress/e2e/edge-firewall/create-edge-firewall-waf.cy.js @@ -13,6 +13,7 @@ const createWAFCase = () => { // Act cy.get(selectors.wafs.nameInput).type(wafName) + cy.intercept('GET', '/api/v3/waf/rulesets/*').as('wafRules') cy.get(selectors.form.actionsSubmitButton).click() cy.verifyToast('success', 'Your waf rule has been created') } @@ -27,7 +28,7 @@ describe('Edge Firewall spec', { tags: ['@dev5'] }, () => { it('should create an Edge Firewall with a rules engine using a WAF', () => { createWAFCase() - cy.wait(2000) + cy.wait('@wafRules') cy.openProduct('Edge Firewall') // Act - create Edge Firewall @@ -96,4 +97,4 @@ describe('Edge Firewall spec', { tags: ['@dev5'] }, () => { cy.verifyToast('Edge Firewall successfully deleted') }) }) -}) \ No newline at end of file +}) diff --git a/src/helpers/metrics-playground-opener.js b/src/helpers/metrics-playground-opener.js index 9cf9b4ef6..34990971e 100644 --- a/src/helpers/metrics-playground-opener.js +++ b/src/helpers/metrics-playground-opener.js @@ -1,6 +1,6 @@ import { getStaticUrlsByEnvironment } from './get-static-urls-by-environment' export const metricsPlaygroundOpener = () => { - const playgroundUrl = getStaticUrlsByEnvironment('playground') + const playgroundUrl = getStaticUrlsByEnvironment('playgroundMetrics') window.open(playgroundUrl, '_blank') } diff --git a/src/modules/real-time-metrics/chart/format-c3-graph-props.js b/src/modules/real-time-metrics/chart/format-c3-graph-props.js index 801ad83b2..e3daf490e 100644 --- a/src/modules/real-time-metrics/chart/format-c3-graph-props.js +++ b/src/modules/real-time-metrics/chart/format-c3-graph-props.js @@ -6,7 +6,7 @@ import { CHART_RULES } from '@modules/real-time-metrics/constants' * @param {any} date - The input to be checked * @returns {boolean} - Returns true if the input is a valid date, otherwise returns false */ -function isDate(date) { +export function isDate(date) { const series = date // eslint-disable-next-line eqeqeq return new Date(series) != 'Invalid Date' @@ -42,7 +42,7 @@ function formatC3DataProp(chartData, resultChart) { const field = resultChart[1][0] data.types = { [field]: 'bar' } - data.labels = true + data.labels = { format: (value) => formatYAxisLabels(value, chartData) } data.color = (_, data) => CHART_RULES.BASE_COLOR_PATTERNS[data.index] return data @@ -63,7 +63,7 @@ function formatC3DataProp(chartData, resultChart) { * @param {Array} resultChart - The result chart * @returns {Object} - Returns the formatted C3 X Axis */ -function formatC3XAxis(chartData, resultChart) { +export function formatC3XAxis(chartData, resultChart) { const isSeriesDate = isDate(resultChart[0][1]) const isSeriesNumeric = isNumeric(resultChart) const isTimeSeries = chartData.xAxis === 'ts' @@ -166,7 +166,7 @@ function formatDataUnit(data) { * @param {Object} chartData - The chart data * @returns {string} - Returns the formatted Y axis labels for the C3 chart */ -function formatYAxisLabels(data, chartData) { +export function formatYAxisLabels(data, chartData) { if (chartData.dataUnit === 'bytes' || chartData.dataUnit === 'bitsPerSecond') { return formatBytesDataUnit(data, chartData) } @@ -187,7 +187,7 @@ function formatYAxisLabels(data, chartData) { * @param {Object} chartData - The chart data * @returns {Object} - Returns the formatted Y axis of the C3 chart */ -function formatC3YAxis(chartData) { +export function formatC3YAxis(chartData, hasCount = true) { const hiddenTypes = ['ordered-bar'] if (hiddenTypes.includes(chartData.type)) { @@ -199,12 +199,15 @@ function formatC3YAxis(chartData) { * Configuration of the Y axis tick */ tick: { - count: CHART_RULES.MAX_COUNT, format: (d) => formatYAxisLabels(d, chartData) }, min: !isRotated ? CHART_RULES.RESET_COUNT : undefined, padding: { bottom: 0 } } + + if (hasCount) { + yAxis.tick.count = CHART_RULES.MAX_COUNT + } if (chartData.maxYAxis) { yAxis.max = chartData.maxYAxis yAxis.padding.top = 0 @@ -249,7 +252,7 @@ function generateMeanLineValues(resultChart, mean) { * @param {string} text - The camelCase string to convert * @returns {string} - The title case string */ -function camelToTitle(text) { +export function camelToTitle(text) { const title = text.replace(/([A-Z])/g, ' $1').replace(/([a-zA-Z])(\d+)/g, '$1 $2') return title.charAt(0).toUpperCase() + title.slice(1) } @@ -284,7 +287,7 @@ function setMeanSeriesValues(serie, seriesTotal, chartData) { * @param {boolean} hasMeanLineTotal - Indicates if the chart has mean line total * @returns {Object} - Returns the series names, mean line total, and mean line series for the C3 chart */ -function getSeriesInfos(resultChart, chartData, hasMeanLineSeries, hasMeanLineTotal) { +export function getSeriesInfos(resultChart, chartData, hasMeanLineSeries, hasMeanLineTotal) { const sliced = resultChart.slice(1) let seriesNames = {} @@ -387,7 +390,7 @@ function displayLegend(chartData) { * @param {boolean} options.hasMeanLineTotal - Flag indicating if the chart has mean line total * @returns {Object} The formatted C3 graph properties */ -export default function FormatC3GraphProps({ +export function FormatC3GraphProps({ chartData, resultChart, hasMeanLineSeries = false, @@ -423,6 +426,10 @@ export default function FormatC3GraphProps({ data.columns = [...data.columns, ...meanLineSeries] } + const showFocus = (chartData) => { + return chartData.type !== 'ordered-bar' + } + const c3Props = { data, axis: { @@ -436,6 +443,9 @@ export default function FormatC3GraphProps({ grid: { y: { show: displayGrid(chartData) + }, + focus: { + show: showFocus(chartData) } }, point: { @@ -452,7 +462,10 @@ export default function FormatC3GraphProps({ ) }, format: { - title: (d) => (isDate(d) ? new Date(d).toLocaleString('en-US') : d) + title: (d) => (isDate(d) ? new Date(d).toLocaleString('en-US') : d), + value: function (value) { + return formatYAxisLabels(value, chartData) + } } }, zoom: { diff --git a/src/modules/real-time-metrics/chart/format-graph.js b/src/modules/real-time-metrics/chart/format-graph.js new file mode 100644 index 000000000..74e9f7e80 --- /dev/null +++ b/src/modules/real-time-metrics/chart/format-graph.js @@ -0,0 +1,35 @@ +import { CHART_RULES } from '@modules/real-time-metrics/constants' + +/** + * Format data for displaying byte unit + * @param {number} data - The data to be formatted + * @param {Object} chartData - The chart data + * @returns {string} - Returns the formatted data for byte unit display + */ +export function formatBytesDataUnit(data, chartData) { + let unit = 'byte' + let value = data + + if (chartData.dataUnit === 'bitsPerSecond' || chartData.dataUnit === 'bites') { + return `${parseFloat(value).toFixed(1)} bits/s` + } + + if (data > CHART_RULES.DATA_VOLUME.tera) { + value = data / CHART_RULES.DATA_VOLUME.tera + unit = `tb/s` + } else if (data > CHART_RULES.DATA_VOLUME.giga) { + value = data / CHART_RULES.DATA_VOLUME.giga + unit = `gb/s` + } else if (data > CHART_RULES.DATA_VOLUME.mega) { + value = data / CHART_RULES.DATA_VOLUME.mega + unit = `mb/s` + } else if (data > CHART_RULES.DATA_VOLUME.kilo) { + value = data / CHART_RULES.DATA_VOLUME.kilo + unit = `kb/s` + } + + return { + value: parseFloat(value).toFixed(1), + unit + } +} diff --git a/src/modules/real-time-metrics/chart/index.js b/src/modules/real-time-metrics/chart/index.js index afd69dc40..e51ad3f61 100644 --- a/src/modules/real-time-metrics/chart/index.js +++ b/src/modules/real-time-metrics/chart/index.js @@ -1,3 +1,17 @@ -import FormatC3GraphProps from './format-c3-graph-props' +import { + FormatC3GraphProps, + formatYAxisLabels, + getSeriesInfos, + formatC3YAxis, + isDate, + camelToTitle +} from './format-c3-graph-props' -export { FormatC3GraphProps } +export { + FormatC3GraphProps, + formatYAxisLabels, + getSeriesInfos, + formatC3YAxis, + isDate, + camelToTitle +} diff --git a/src/modules/real-time-metrics/helpers/countries-code.json b/src/modules/real-time-metrics/helpers/countries-code.json new file mode 100644 index 000000000..70be47ead --- /dev/null +++ b/src/modules/real-time-metrics/helpers/countries-code.json @@ -0,0 +1,197 @@ +{ + "Afghanistan": "af", + "Albania": "al", + "Algeria": "dz", + "Andorra": "ad", + "Angola": "ao", + "Antigua and Barbuda": "ag", + "Argentina": "ar", + "Armenia": "am", + "Australia": "au", + "Austria": "at", + "Azerbaijan": "az", + "Bahamas": "bs", + "Bahrain": "bh", + "Bangladesh": "bd", + "Barbados": "bb", + "Belarus": "by", + "Belgium": "be", + "Belize": "bz", + "Benin": "bj", + "Bhutan": "bt", + "Bolivia": "bo", + "Bosnia and Herzegovina": "ba", + "Botswana": "bw", + "Brazil": "br", + "Brunei": "bn", + "Bulgaria": "bg", + "Burkina Faso": "bf", + "Burundi": "bi", + "Cabo Verde": "cv", + "Cambodia": "kh", + "Cameroon": "cm", + "Canada": "ca", + "Central African Republic": "cf", + "Chad": "td", + "Chile": "cl", + "China": "cn", + "Colombia": "co", + "Comoros": "km", + "Congo (Congo-Brazzaville)": "cg", + "Costa Rica": "cr", + "Croatia": "hr", + "Cuba": "cu", + "Cyprus": "cy", + "Czechia (Czech Republic)": "cz", + "Democratic Republic of the Congo": "cd", + "Denmark": "dk", + "Djibouti": "dj", + "Dominica": "dm", + "Dominican Republic": "do", + "Ecuador": "ec", + "Egypt": "eg", + "El Salvador": "sv", + "Equatorial Guinea": "gq", + "Eritrea": "er", + "Estonia": "ee", + "Swaziland": "sz", + "Ethiopia": "et", + "Fiji": "fj", + "Finland": "fi", + "France": "fr", + "Gabon": "ga", + "Gambia": "gm", + "Georgia": "ge", + "Germany": "de", + "Ghana": "gh", + "Greece": "gr", + "Grenada": "gd", + "Guatemala": "gt", + "Guinea": "gn", + "Guinea-Bissau": "gw", + "Guyana": "gy", + "Haiti": "ht", + "Honduras": "hn", + "Hungary": "hu", + "Iceland": "is", + "India": "in", + "Indonesia": "id", + "Iran": "ir", + "Iraq": "iq", + "Ireland": "ie", + "Israel": "il", + "Italy": "it", + "Jamaica": "jm", + "Japan": "jp", + "Jordan": "jo", + "Kazakhstan": "kz", + "Kenya": "ke", + "Kiribati": "ki", + "Kuwait": "kw", + "Kyrgyzstan": "kg", + "Laos": "la", + "Latvia": "lv", + "Lebanon": "lb", + "Lesotho": "ls", + "Liberia": "lr", + "Libya": "ly", + "Liechtenstein": "li", + "Lithuania": "lt", + "Luxembourg": "lu", + "Madagascar": "mg", + "Malawi": "mw", + "Malaysia": "my", + "Maldives": "mv", + "Mali": "ml", + "Malta": "mt", + "Marshall Islands": "mh", + "Mauritania": "mr", + "Mauritius": "mu", + "Mexico": "mx", + "Micronesia": "fm", + "Moldova": "md", + "Monaco": "mc", + "Mongolia": "mn", + "Montenegro": "me", + "Morocco": "ma", + "Mozambique": "mz", + "Myanmar (formerly Burma)": "mm", + "Namibia": "na", + "Nauru": "nr", + "Nepal": "np", + "Netherlands": "nl", + "New Zealand": "nz", + "Nicaragua": "ni", + "Niger": "ne", + "Nigeria": "ng", + "North Korea": "kp", + "North Macedonia": "mk", + "Norway": "no", + "Oman": "om", + "Pakistan": "pk", + "Palau": "pw", + "Palestine State": "ps", + "Panama": "pa", + "Papua New Guinea": "pg", + "Paraguay": "py", + "Peru": "pe", + "Philippines": "ph", + "Poland": "pl", + "Portugal": "pt", + "Qatar": "qa", + "Romania": "ro", + "Russia": "ru", + "Rwanda": "rw", + "Saint Kitts and Nevis": "kn", + "Saint Lucia": "lc", + "Saint Vincent and the Grenadines": "vc", + "Samoa": "ws", + "San Marino": "sm", + "Sao Tome and Principe": "st", + "Saudi Arabia": "sa", + "Senegal": "sn", + "Serbia": "rs", + "Seychelles": "sc", + "Sierra Leone": "sl", + "Singapore": "sg", + "Slovakia": "sk", + "Slovenia": "si", + "Solomon Islands": "sb", + "Somalia": "so", + "South Africa": "za", + "South Korea": "kr", + "South Sudan": "ss", + "Spain": "es", + "Sri Lanka": "lk", + "Sudan": "sd", + "Suriname": "sr", + "Sweden": "se", + "Switzerland": "ch", + "Syria": "sy", + "Taiwan": "tw", + "Tajikistan": "tj", + "Tanzania": "tz", + "Thailand": "th", + "Timor-Leste": "tl", + "Togo": "tg", + "Tonga": "to", + "Trinidad and Tobago": "tt", + "Tunisia": "tn", + "Turkey": "tr", + "Turkmenistan": "tm", + "Tuvalu": "tv", + "Uganda": "ug", + "Ukraine": "ua", + "United Arab Emirates": "ae", + "United Kingdom": "gb", + "United States": "us", + "Uruguay": "uy", + "Uzbekistan": "uz", + "Vanuatu": "vu", + "Vatican City": "va", + "Venezuela": "ve", + "Vietnam": "vn", + "Yemen": "ye", + "Zambia": "zm", + "Zimbabwe": "zw" +} diff --git a/src/modules/real-time-metrics/reports/convert-beholder-to-chart.js b/src/modules/real-time-metrics/reports/convert-beholder-to-chart.js index eda3859fe..f459552b5 100644 --- a/src/modules/real-time-metrics/reports/convert-beholder-to-chart.js +++ b/src/modules/real-time-metrics/reports/convert-beholder-to-chart.js @@ -1,4 +1,126 @@ +/* eslint-disable id-length */ + import { CHART_RULES } from '@modules/real-time-metrics/constants' +import { formatBytesDataUnit } from '../chart/format-graph' +import countries from '../helpers/countries-code.json' + +import { + formatYAxisLabels, + getSeriesInfos, + formatC3YAxis, + isDate, + camelToTitle +} from '@modules/real-time-metrics/chart' + +const COLOR_PATTERNS = { + color: { + pattern: [ + 'var(--series-one-color)', + 'var(--series-two-color)', + 'var(--series-three-color)', + 'var(--series-four-color)', + 'var(--series-five-color)', + 'var(--series-six-color)', + 'var(--series-seven-color)', + 'var(--series-eight-color)', + 'var(--series-one-color)', + 'var(--series-two-color)', + 'var(--series-three-color)', + 'var(--series-four-color)', + 'var(--series-five-color)', + 'var(--series-six-color)', + 'var(--series-seven-color)', + 'var(--series-eight-color)' + ] + } +} + +const objectStackedChart = ({ columns, seriesNames, report, type }) => { + return [ + { + id: crypto.randomUUID().toString(), + data: { + x: 'x', + columns, + type, + groups: [report.fields], + names: seriesNames + }, + tooltip: { + format: { + title: (d) => (isDate(d) ? new Date(d).toLocaleString('en-US') : d), + name: (name, ratio, id) => { + return camelToTitle(id) + }, + value: function (value) { + return formatYAxisLabels(value, report) + } + } + }, + legend: { + hide: false, + position: 'bottom' + }, + axis: { + x: { + type: 'timeseries', + localtime: false, + tick: { + format: '%b-%d %H:%M', + width: 40 + } + }, + y: formatC3YAxis(report, false) + }, + padding: { + bottom: 16, + right: 30 + }, + grid: { + y: { + lines: [{ value: 0 }] + } + }, + bar: { + width: { + ratio: 0.25 + } + }, + ...COLOR_PATTERNS + } + ] +} + +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ +const handleStackedData = ({ report, data }) => { + const dataset = Object.keys(data) + const timestamps = data[dataset].map((entry) => entry[report.xAxis]).sort() + + const rows = report.fields.map((field) => { + return [ + field, + ...timestamps.map((ts) => { + const entry = data[dataset].find((item) => item[report.xAxis] === ts) + return entry ? entry[field] : 0 + }) + ] + }) + + const headerChart = ['x', ...timestamps.map((ts) => new Date(ts))] + const columns = [headerChart, ...rows] + + const { seriesNames } = getSeriesInfos(columns, report, false, false) + + return { + columns, + seriesNames + } +} /** * Fills the series with zeroes to make them all the same length. @@ -177,7 +299,7 @@ const formatTsChartData = ({ } series[key] = [key] - /* + /* if the series is new and there are already records of other series, it needs to be filled with zero values to ensure correct display in the REPORTS */ if (countValues > 0) { @@ -230,6 +352,81 @@ const formatCatAbsoluteChartData = ({ report, data }) => { }) } +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ + +const formatStackedBarChart = ({ report, data }) => { + const { columns, seriesNames } = handleStackedData({ report, data }) + const type = 'bar' + return objectStackedChart({ columns, seriesNames, report, type }) +} + +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ + +const formatGaugeChart = ({ report, data }) => { + const dataset = Object.keys(data) + const geolocCountryName = report.groupBy[0] + const columnName = data[dataset][0][geolocCountryName] + const fieldName = report.fields[0] + + const totalGaugeValue = data[dataset].reduce((acc, current) => acc + current[fieldName], 0) + const threshold = report.threshold || [30, 60, 90] + const maxSupportedValue = threshold[threshold.length - 1] + + return [ + { + id: crypto.randomUUID().toString(), + data: { + columns: [[columnName, totalGaugeValue]], + type: 'gauge' + }, + gauge: { + label: { + format: function (value) { + return `${formatYAxisLabels(value, report)}` + }, + show: false + }, + max: maxSupportedValue + }, + color: { + pattern: [ + 'var(--scale-red)', + 'var(--scale-orange)', + 'var(--scale-yellow)', + 'var(--scale-green)' + ], + threshold: { + values: threshold + } + }, + tooltip: { + format: { + value: function (value) { + return formatYAxisLabels(value, report) + } + } + } + } + ] +} + +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ + const formatRotatedBarChartData = ({ report, data }) => { const dataset = Object.keys(data) const seriesName = report.groupBy[0] @@ -246,6 +443,69 @@ const formatRotatedBarChartData = ({ report, data }) => { return [series, values] } +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ + +const formatBigNumbers = ({ report, data }) => { + const dataset = Object.keys(data) + const fieldName = report.fields[0] + + const total = data[dataset].reduce((acc, current) => acc + current[fieldName], 0) + const { unit, value } = formatBytesDataUnit(total, report) + + return [ + { + value, + variationType: report.variationType, + unit + } + ] +} + +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ +const formatListChart = ({ report, data }) => { + const dataset = Object.keys(data) + const fieldsRequest = Object.keys(data[dataset][0]) + const fieldNames = report.fields + const fieldCountryName = report.groupBy[0] + + const dataValue = data[dataset].map((obj) => { + const extractedObj = {} + fieldNames.forEach((key) => { + extractedObj[key] = formatYAxisLabels(obj[key], report) + extractedObj[fieldCountryName] = { + code: countries[obj[fieldCountryName]] || '-', + country: obj[fieldCountryName] + } + }) + + return { ...obj, ...extractedObj } + }) + + const header = fieldsRequest.map((field) => camelToTitle(field)) + + const columns = fieldsRequest.map((field, index) => ({ + field: field, + header: header[index] + })) + + return [ + { + data: dataValue, + columns + } + ] +} + const formatMapChartData = ({ report, data }) => { const dataset = Object.keys(data) const geolocCountryName = report.groupBy[0] @@ -269,6 +529,18 @@ const formatMapChartData = ({ report, data }) => { ] } +/** + * Formats pie chart data based on the provided report and data. + * + * @param {Object} report - The report object containing chart configuration. + * @param {Array} data - The data to be formatted. + */ +const formatStackedAreaChart = ({ report, data }) => { + const { columns, seriesNames } = handleStackedData({ report, data }) + const type = 'area' + return objectStackedChart({ columns, seriesNames, report, type }) +} + /** * Function that transforms a list of tuples into a list of lists (columns). * @@ -309,6 +581,16 @@ function ConvertBeholderToChart({ return formatRotatedBarChartData({ report, data }) case 'map': return formatMapChartData({ report, data }) + case 'big-numbers': + return formatBigNumbers({ report, data }) + case 'list': + return formatListChart({ report, data }) + case 'gauge': + return formatGaugeChart({ report, data }) + case 'stacked-area': + return formatStackedAreaChart({ report, data }) + case 'stacked-bar': + return formatStackedBarChart({ report, data }) default: return [] } diff --git a/src/plugins/factories/stripe-integration-factory.js b/src/plugins/factories/stripe-integration-factory.js index 0dd63a3fc..34d91ae11 100644 --- a/src/plugins/factories/stripe-integration-factory.js +++ b/src/plugins/factories/stripe-integration-factory.js @@ -21,7 +21,10 @@ export function makeStripeClient(environment) { if (!stripeToken) { throw Error('Stripe token is missing, cannot load Stripe. View readme for more info.') } - const stripePromise = loadStripe(stripeToken) + + const stripePromise = loadStripe(stripeToken, { + locale: 'en' + }) return stripePromise } diff --git a/src/router/hooks/redirectToManager.js b/src/router/hooks/redirectToManager.js index ea5c665e3..2af37ab58 100644 --- a/src/router/hooks/redirectToManager.js +++ b/src/router/hooks/redirectToManager.js @@ -1,6 +1,7 @@ import { getEnvironment, getStaticUrlsByEnvironment } from '@/helpers' import { loadContractServicePlan } from '@/services/contract-services' import { useAccountStore } from '@/stores/account' +import { listClientIdsReleasedForConsoleService } from '@/services/account-services' /** @type {import('vue-router').NavigationGuardWithThis} */ export default async function redirectToManager(to, __, next) { @@ -28,12 +29,15 @@ export default async function redirectToManager(to, __, next) { } // account that are kind client, can access with developer service plan - const { isDeveloperSupportPlan } = await loadContractServicePlan({ - clientId: accountData.client_id - }) + const [{ isDeveloperSupportPlan }, clientIdsReleadesForConsole] = await Promise.all([ + loadContractServicePlan({ clientId: accountData.client_id }), + listClientIdsReleasedForConsoleService() + ]) accountStore.setAccountData({ isDeveloperSupportPlan: isDeveloperSupportPlan }) - if (!isDeveloperSupportPlan) { + if (clientIdsReleadesForConsole.includes(accountData.client_id)) { + return next() + } permanentRedirectToManager() } } diff --git a/src/services/account-services/index.js b/src/services/account-services/index.js index e62e74438..3cdcb6090 100644 --- a/src/services/account-services/index.js +++ b/src/services/account-services/index.js @@ -1,4 +1,5 @@ import { getUserInfoService } from './get-user-info-service' import { getAccountInfoService } from './get-account-info-service' +import { listClientIdsReleasedForConsoleService } from './list-client-ids-released-for-console-service' -export { getAccountInfoService, getUserInfoService } +export { getAccountInfoService, getUserInfoService, listClientIdsReleasedForConsoleService } diff --git a/src/services/account-services/list-client-ids-released-for-console-service.js b/src/services/account-services/list-client-ids-released-for-console-service.js new file mode 100644 index 000000000..e2086f819 --- /dev/null +++ b/src/services/account-services/list-client-ids-released-for-console-service.js @@ -0,0 +1,17 @@ +import { AxiosHttpClientAdapter } from '../axios/AxiosHttpClientAdapter' +import { getEnvironment } from '@/helpers/get-environment' + +export const listClientIdsReleasedForConsoleService = async () => { + const httpResponse = await AxiosHttpClientAdapter.request({ + url: `/allowed-accounts`, + method: 'GET' + }) + return adapt(httpResponse) +} + +const adapt = (httpResponse) => { + const environment = getEnvironment() + const clientIds = httpResponse?.body?.[environment]?.client_ids + + return clientIds ?? [] +} diff --git a/src/services/edge-application-device-groups-services/create-device-group-service.js b/src/services/edge-application-device-groups-services/create-device-group-service.js index 5e51803fa..da56e3bb4 100644 --- a/src/services/edge-application-device-groups-services/create-device-group-service.js +++ b/src/services/edge-application-device-groups-services/create-device-group-service.js @@ -4,7 +4,7 @@ import * as Errors from '@/services/axios/errors' export const createDeviceGroupService = async (payload) => { const { edgeApplicationId } = payload - let httpResponse = await AxiosHttpClientAdapter.request({ + const httpResponse = await AxiosHttpClientAdapter.request({ url: `${makeEdgeApplicationBaseUrl()}/${edgeApplicationId}/device_groups`, method: 'POST', body: adapt(payload) diff --git a/src/services/edge-application-device-groups-services/edit-device-group-service.js b/src/services/edge-application-device-groups-services/edit-device-group-service.js index 841ada6b4..4724b6f25 100644 --- a/src/services/edge-application-device-groups-services/edit-device-group-service.js +++ b/src/services/edge-application-device-groups-services/edit-device-group-service.js @@ -3,7 +3,7 @@ import { makeEdgeApplicationBaseUrl } from '../edge-application-services/make-ed import * as Errors from '@/services/axios/errors' export const editDeviceGroupService = async (payload) => { - let httpResponse = await AxiosHttpClientAdapter.request({ + const httpResponse = await AxiosHttpClientAdapter.request({ url: `${makeEdgeApplicationBaseUrl()}/${payload.edgeApplicationId}/device_groups/${payload.id}`, method: 'PATCH', body: adapt(payload) diff --git a/src/templates/add-payment-method-block/index.vue b/src/templates/add-payment-method-block/index.vue index a4f507bde..9a53e9ee0 100644 --- a/src/templates/add-payment-method-block/index.vue +++ b/src/templates/add-payment-method-block/index.vue @@ -2,17 +2,20 @@ import { computed, ref, inject, onMounted } from 'vue' import { useToast } from 'primevue/usetoast' import ActionBarBlock from '@/templates/action-bar-block' - import GoBack from '@/templates/action-bar-block/go-back' import Sidebar from 'primevue/sidebar' import { useAccountStore } from '@/stores/account' import FeedbackFish from '@/templates/navbar-block/feedback-fish' import InlineMessage from 'primevue/inlinemessage' import FormHorizontal from '@/templates/create-form-block/form-horizontal' import LabelBlock from '@/templates/label-block' - defineOptions({ - name: 'add-payment-method-block' - }) + import { useScrollToError } from '@/composables/useScrollToError' + import InputText from 'primevue/inputtext' + import { useField } from 'vee-validate' + import * as yup from 'yup' + + defineOptions({ name: 'add-payment-method-block' }) const stripePlugin = inject('stripe') + const accountStore = useAccountStore() const stripe = ref(null) const isSubmitting = ref(false) @@ -20,8 +23,20 @@ const cardNumber = ref(null) const cardExpiry = ref(null) const cardCvc = ref(null) - const cardholderName = ref('') - + const displayError = ref({}) + const { scrollToError } = useScrollToError() + const MESSAGE_INPUTS_STRIPE = { + invalid: { + cardNumber: 'Invalid card number', + cardExpiry: 'Invalid expiration date', + cardCvc: 'Invalid security code' + }, + empty: { + cardNumber: 'Card number is required', + cardExpiry: 'Expiration date is required', + cardCvc: 'Security code is required' + } + } const emit = defineEmits(['update:visible', 'onSuccess', 'onError']) const props = defineProps({ createService: { @@ -31,13 +46,20 @@ }) const toast = useToast() - const showGoBack = ref(false) onMounted(async () => { await initializeStripeComponents() addStripeComponentsToTemplate() }) + const { + value: cardholderName, + validate, + errorMessage: errorCardholderName + } = useField('cardholderName', yup.string().required('Your card holder name is required'), { + initialValue: '' + }) + const initializeStripeComponents = async () => { stripe.value = await stripePlugin stripeComponents.value = stripe.value.elements() @@ -65,20 +87,48 @@ cardCvc.value = stripeComponents.value.create('cardCvc', inputStyles) } + const handleError = (event, errorType) => { + const { error } = event + delete displayError.value[errorType] + + if (error) { + displayError.value[errorType] = error.message + } + } + + const handleBlur = (event) => { + const element = stripeComponents.value.getElement(event.elementType) + if (element._empty) { + displayError.value[event.elementType] = MESSAGE_INPUTS_STRIPE.empty[event.elementType] + return + } + if (element._invalid) { + displayError.value[event.elementType] = MESSAGE_INPUTS_STRIPE.invalid[event.elementType] + } + } + const addStripeComponentsToTemplate = () => { cardNumber.value.mount('#card-number-element') + cardNumber.value.on('change', (event) => handleError(event, 'cardNumber')) + cardNumber.value.on('blur', handleBlur) + cardExpiry.value.mount('#card-expiry-element') + cardExpiry.value.on('change', (event) => handleError(event, 'cardExpiry')) + cardExpiry.value.on('blur', handleBlur) + cardCvc.value.mount('#card-cvc-element') + cardCvc.value.on('change', (event) => handleError(event, 'cardCvc')) + cardCvc.value.on('blur', handleBlur) } const visibleDrawer = computed({ get: () => props.visible, set: (value) => { - changeVisisbleDrawer(value, true) + changeVisibleDrawer(value, true) } }) - const changeVisisbleDrawer = (isVisible) => { + const changeVisibleDrawer = (isVisible) => { emit('update:visible', isVisible) } @@ -90,6 +140,14 @@ toggleDrawerVisibility(false) } + const validateCardholderName = async () => { + await validate() + delete displayError.value.cardholderName + if (errorCardholderName.value) { + displayError.value.cardholderName = errorCardholderName.value + } + } + const showToast = (severity, summary) => { const options = { closable: true, @@ -104,43 +162,39 @@ const handleSubmit = async () => { isSubmitting.value = true try { - const { token, error: submitionErrors } = await stripe.value.createToken(cardNumber.value, { + await validateCardholderName() + const { token, error: hasErrors } = await stripe.value.createToken(cardNumber.value, { name: cardholderName.value }) - if (submitionErrors !== undefined) { - showToast('error', submitionErrors.message) + + if (hasErrors || errorCardholderName.value) { + scrollToError(displayError.value) + return } - if (submitionErrors === undefined) { - const accountData = accountStore.account - const payload = { - card_address_zip: accountData.postal_code, - card_country: accountData.country, - stripe_token: token.id, - card_id: token.card.id, - card_brand: token.card.brand, - card_holder: token.card.name, - card_last_4_digits: token.card.last4, - card_expiration_month: token.card.exp_month, - card_expiration_year: token.card.exp_year - } - const response = await props.createService(payload) - emit('onSuccess', response) - showToast('success', response.feedback) + + const accountData = accountStore.account + const payload = { + card_address_zip: accountData.postal_code, + card_country: accountData.country, + stripe_token: token.id, + card_id: token.card.id, + card_brand: token.card.brand, + card_holder: token.card.name, + card_last_4_digits: token.card.last4, + card_expiration_month: token.card.exp_month, + card_expiration_year: token.card.exp_year } + const response = await props.createService(payload) + emit('onSuccess', response) + showToast('success', response.feedback) + toggleDrawerVisibility(false) } catch (error) { emit('onError', error) showToast('error', error) } finally { - showGoBack.value = props.showBarGoBack - toggleDrawerVisibility(false) isSubmitting.value = false } } - - const handleGoBack = () => { - showGoBack.value = false - toggleDrawerVisibility(false) - }