diff --git a/app/assets/javascripts/behaviors/components/json_chart.vue b/app/assets/javascripts/behaviors/components/json_chart.vue new file mode 100644 index 0000000000000000000000000000000000000000..4d43181eb2d21f742d25a40ce663009c45cdf074 --- /dev/null +++ b/app/assets/javascripts/behaviors/components/json_chart.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index fbc4fcdcd394a0394cfb9a4dc9130de55b0cd9a8..515c734ea1e02d9035993c069bd08f48fcb79e43 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -5,6 +5,7 @@ import renderMath from './render_math'; import renderSandboxedMermaid from './render_sandboxed_mermaid'; import { renderGlql } from './render_glql'; import { renderJSONTable, renderJSONTableHTML } from './render_json_table'; +import { renderJSONChart } from './render_json_chart'; import { addAriaLabels } from './accessibility'; function initPopovers(elements) { @@ -29,6 +30,7 @@ export function renderGFM(element) { mermaidEls, tableEls, tableHTMLEls, + chartEls, glqlEls, userEls, popoverEls, @@ -40,6 +42,7 @@ export function renderGFM(element) { '.js-render-mermaid', '[data-canonical-lang="json"][data-lang-params~="table"]', 'table[data-table-fields]', + '[data-canonical-lang="json"][data-lang-params~="chart"]', '[data-canonical-lang="glql"]', '.gfm-project_member', '.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic, .gfm-milestone', @@ -52,6 +55,7 @@ export function renderGFM(element) { renderSandboxedMermaid(mermaidEls); renderJSONTable(tableEls.map((e) => e.parentNode)); renderJSONTableHTML(tableHTMLEls); + renderJSONChart(chartEls.map((e) => e.parentNode)); highlightCurrentUser(userEls); initPopovers(popoverEls); addAriaLabels(taskListCheckboxEls); diff --git a/app/assets/javascripts/behaviors/markdown/render_json_chart.js b/app/assets/javascripts/behaviors/markdown/render_json_chart.js new file mode 100644 index 0000000000000000000000000000000000000000..c00a53f2ab755ef220a322e4d3cd2b4d1b2f9355 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/render_json_chart.js @@ -0,0 +1,86 @@ +// Follows the example of `render_json_table.js` in order to detect +// and render json data as various types of charts. The intent is to +// support the same json structure as our GitLab UI chart components, such as +// https://design.gitlab.com/storybook/?path=/docs/charts-line-chart--docs +// +// Example markdown: +// +// ```json:chart +// { +// "type" : "line", +// "data" : [ +// {"name" : "First Series", +// "data" : [ +// ["Mon",1220],["Tue",932],["Wed",901],["Thu",934],["Fri",1290],["Sat",1330],["Sun",1320] +// ] +// } +// ], +// "option": {"xAxis":{"name":"Time","type":"category"}} +// } +// ``` + +import { memoize } from 'lodash'; +import Vue from 'vue'; +import { __ } from '~/locale'; +import { createAlert } from '~/alert'; + +// Async import component since we might not need it... +const JSONChart = memoize( + () => import(/* webpackChunkName: 'gfm_json_chart' */ '../components/json_chart.vue'), +); + +const mountParseError = (element) => { + // Let the error container be a sibling to the element. + // Otherwise, dismissing the alert causes the copy button to be misplaced. + const container = document.createElement('div'); + element.insertAdjacentElement('beforebegin', container); + + // We need to create a child element with a known selector for `createAlert` + const el = document.createElement('div'); + el.classList.add('js-json-chart-error'); + + container.insertAdjacentElement('afterbegin', el); + + return createAlert({ + message: __('Unable to parse JSON'), + variant: 'warning', + parent: container, + containerSelector: '.js-json-chart-error', + }); +}; + +const mountJSONChartVueComponent = (userData, element) => { + const container = document.createElement('div'); + + element.classList.add('js-json-chart'); + element.replaceChildren(container); + + const props = { + chartType: userData.type, + chartConfig: userData, + }; + + return new Vue({ + el: container, + render(h) { + return h(JSONChart, { props }); + }, + }); +}; + +const renderChart = (element) => { + // Avoid rendering multiple times + if (!element || element.classList.contains('js-json-chart')) { + return; + } + + try { + mountJSONChartVueComponent(JSON.parse(element.textContent), element); + } catch (e) { + mountParseError(element); + } +}; + +export const renderJSONChart = (elements) => { + elements.forEach(renderChart); +};