diff options
author | Jose Vargas <jvargas@gitlab.com> | 2019-05-02 17:12:02 -0500 |
---|---|---|
committer | Jose Vargas <jvargas@gitlab.com> | 2019-05-02 17:12:02 -0500 |
commit | eb86549f45402c9fabbbf4ba98330ffa9e9da7bb (patch) | |
tree | f0eef1807cf18c0c82371fc6db47b4681565e022 | |
parent | 92cdf61672f25805fff4dd245c7c3eb3749c0d6f (diff) | |
download | gitlab-ce-eb86549f45402c9fabbbf4ba98330ffa9e9da7bb.tar.gz |
Add fetchDeployments and fetchEnvironments actions
Also moved the monitoring store functions to an utils.js file.
Some changes are commented as they're still early WIP
7 files changed, 197 insertions, 40 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index c1ca08862d2..4740c37d194 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -103,19 +103,20 @@ export default { }, }, computed: { - ...mapState(['groups']), + ...mapState(['groups', 'emptyState', 'showEmptyState']), }, data() { return { store: new MonitoringStore(), state: 'gettingStarted', - showEmptyState: true, elWidth: 0, selectedTimeWindow: '', }; }, created() { this.setMetricsEndpoint(this.metricsEndpoint); + this.setDeploymentsEndpoint(this.deploymentEndpoint); + this.setEnvironmentsEndpoint(this.environmentsEndpoint); // TODO: Move all of this to the monitoring vuex store/state this.service = new MonitoringService({ @@ -144,7 +145,8 @@ export default { .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), ]; if (!this.hasMetrics) { - this.state = 'gettingStarted'; + // TODO: This should be coming from a mutation/computedGetter + // this.state = 'gettingStarted'; } else { if (this.environmentsEndpoint) { this.servicePromises.push( @@ -157,12 +159,8 @@ export default { ); } // TODO: Use this instead of the monitoring_service methods - this.fetchMetricsData(getTimeDiff(this.timeWindows.eightHours)) - .then((resp) => { - console.log('groups from vuex: ', this.groups); - }).catch((err) => { - }); - this.getGraphsData(); + this.fetchMetricsData(getTimeDiff(this.timeWindows.eightHours)); + // this.getGraphsData(); sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); sidebarMutationObserver.observe(document.querySelector('.layout-page'), { attributes: true, @@ -172,7 +170,12 @@ export default { } }, methods: { - ...mapActions(['fetchMetricsData', 'setMetricsEndpoint']), + ...mapActions([ + 'fetchMetricsData', + 'setMetricsEndpoint', + 'setDeploymentsEndpoint', + 'setEnvironmentsEndpoint', + ]), getGraphAlerts(queries) { if (!this.allAlerts) return {}; const metricIdsForChart = queries.map(q => q.metricId); @@ -182,23 +185,23 @@ export default { return Object.values(this.getGraphAlerts(queries)); }, getGraphsData() { - this.state = 'loading'; + // this.state = 'loading'; Promise.all(this.servicePromises) .then(() => { if (this.store.groups.length < 1) { - this.state = 'noData'; + // this.state = 'noData'; return; } - this.showEmptyState = false; + // this.showEmptyState = false; TODO: Delete me }) .catch(() => { - this.state = 'unableToConnect'; + // this.state = 'unableToConnect'; }); }, getGraphsDataWithTime(timeFrame) { - this.state = 'loading'; - this.showEmptyState = true; + // this.state = 'loading'; + // this.showEmptyState = true; TODO: Delete me! this.service .getGraphsData(getTimeDiff(this.timeWindows[timeFrame])) .then(data => { @@ -209,7 +212,7 @@ export default { Flash(s__('Metrics|Not enough data to display')); }) .finally(() => { - this.showEmptyState = false; + // this.showEmptyState = false; TODO: Delete me! }); }, onSidebarMutation() { @@ -226,7 +229,7 @@ export default { <template> <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> - <div + <!-- <div v-if="environmentsEndpoint" class="dropdowns d-flex align-items-center justify-content-between" > @@ -308,11 +311,12 @@ export default { :graph-data="graphData" /> </template> - </graph-group> + </graph-group> TODO: Uncomment this once the action that requests all data is in place--> + <div><h1>Finished loading...</h1></div> </div> <empty-state v-else - :selected-state="state" + :selected-state="emptyState" :documentation-path="documentationPath" :settings-path="settingsPath" :clusters-path="clustersPath" diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 75cbe9cb2e8..89d88c97feb 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -29,8 +29,15 @@ function backOffRequest(makeRequestCallback) { export const setMetricsEndpoint = ({ commit }, metricsEndpoint) => { commit(types.SET_METRICS_ENDPOINT, metricsEndpoint); -} +}; +export const setDeploymentsEndpoint = ({ commit }, deploymentsEndpoint) => { + commit(types.SET_DEPLOYMENTS_ENDPOINT, deploymentsEndpoint); +}; + +export const setEnvironmentsEndpoint = ({ commit }, environmentsEndpoint ) => { + commit(types.SET_ENVIRONMENTS_ENDPOINT, environmentsEndpoint); +}; export const requestMetricsData = () => ({ commit }) => commit(types.REQUEST_METRICS_DATA); export const receiveMetricsDataSuccess = ({ commit }, data) => @@ -39,17 +46,56 @@ export const receiveMetricsDataFailure = ({ commit }, error) => commit(types.RECEIVE_METRICS_DATA_FAILURE, error); export const fetchMetricsData = ({ state, dispatch }, params) => { + dispatch('requestMetricsData'); + return backOffRequest(() => axios.get(state.metricsEndpoint, { params })) .then(resp => resp.data) .then(response => { if (!response || !response.data || !response.success) { - dispatch('receiveMetricsDataFailure', {}); // TODO: Do we send an error? + dispatch('receiveMetricsDataFailure', null); createFlash(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); } dispatch('receiveMetricsDataSuccess', response.data); }) .catch(error => { - dispatch('receiveMetricsDataFailure', error); // TODO: Do we send an error? + dispatch('receiveMetricsDataFailure', error); + createFlash(s__('Metrics|There was an error while retrieving metrics')); + }); +}; + +export const fetchDeploymentsData = ({ state }) => { + if (!state.deploymentEndpoint) { + return Promise.resolve([]); + } + return backOffRequest(() => axios.get(state.deploymentEndpoint)) + .then(resp => resp.data) + .then(response => { + if (!response || !response.deployments) { + createFlash( + s__('Metrics|Unexpected deployment data response from prometheus endpoint'), + ); + } + return response.deployments; + }) + .catch(() => { + createFlash(s__('Metrics|There was an error getting deployment information.')); + }); +}; + +export const fetchEnvironmentsData = ({ state }) => { + return axios + .get(state.environmentsEndpoint) + .then(resp => resp.data) + .then(response => { + if (!response || !response.environments) { + createFlash( + s__('Metrics|There was an error fetching the environments data, please try again'), + ); + } + return response.environments; + }) + .catch(() => { + createFlash(s__('Metrics|There was an error getting environments information.')); }); }; diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js index e69de29bb2d..2e43b9175b2 100644 --- a/app/assets/javascripts/monitoring/stores/getters.js +++ b/app/assets/javascripts/monitoring/stores/getters.js @@ -0,0 +1,6 @@ +export const getMetricsCount = state => { + return state.groups.reduce((count, group) => count + group.metrics.length, 0); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index a47a5240767..a973f18768e 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -1,5 +1,13 @@ export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA'; export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS'; export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE'; +export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA'; +export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS'; +export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE'; +export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; +export const RECEIVE_ENVIRONMENTS_DATA_SUCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCESS'; +export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE'; export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT'; +export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT'; +export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 74cc3bfca16..76edb2e1a22 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -1,14 +1,21 @@ import * as types from './mutation_types'; +import { normalizeMetrics, sortMetrics } from './utils'; export default { - // I understand now [types.REQUEST_METRICS_DATA](state) { state.emptyState = 'loading'; - state.showEmptyState = true; + // state.showEmptyState = true; }, - [types.RECEIVE_METRICS_DATA_SUCCESS](state, data) { - state.groups = data; // TODO: Transform the data from the backend response - state.showEmptyState = false; + [types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) { + state.groups = groupData.map(group => ({ + ...group, + metrics: normalizeMetrics(sortMetrics(group.metrics)), + })); + if (state.groups.length < 1) { + state.emptyState = 'noData'; + } else { + state.showEmptyState = false; + } }, [types.RECEIVE_METRICS_DATA_FAILURE](state, error) { state.emptyState = error ? 'unableToConnect' : 'noData'; // TODO: use error to deterine the appropiately determine which empty state to use @@ -17,4 +24,10 @@ export default { [types.SET_METRICS_ENDPOINT](state, endpoint) { state.metricsEndpoint = endpoint; }, + [types.SET_ENVIRONMENTS_ENDPOINT](state, endpoint) { + state.environmentsEndpoint = endpoint; + }, + [types.SET_DEPLOYMENTS_ENDPOINT](state, endpoint) { + state.deploymentsEndpoint = endpoint; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 415c0cb0f30..a8f7ab83d5c 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -1,21 +1,17 @@ export default () => ({ hasMetrics: false, showPanels: true, - documentationPath: null, // From the vuex docs, strings should be null by default - settingsPath: null, // From dashboard.vue, props clustersPath: null, tagsPath: null, projectPath: null, + currentEnvironmentName: null, // Finish props metricsEndpoint: null, - deploymentEndpoint: null, - emptyGettingStartedSvgPath: null, - emptyLoadingSvgPath: null, - emptyNoDataSvgPath: null, - emptyUnableToConnectSvgPath: null, environmentsEndpoint: null, - currentEnvironmentName: null, // Finish props - showEmptyState: true, // From the data - emptyState: 'loading', - selectedTimeWindow: null, // finish data section - groups: [], // NEW + deploymentsEndpoint: null, + emptyState: 'gettingStarted', + showEmptyState: true, + selectedTimeWindow: null, + groups: [],// From the monitoring store + deploymentData: [], + environmentsData: [], }); diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js new file mode 100644 index 00000000000..2cce99100ca --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -0,0 +1,84 @@ +import _ from 'underscore'; + +function checkQueryEmptyData(query) { + return { + ...query, + result: query.result.filter(timeSeries => { + const newTimeSeries = timeSeries; + const hasValue = series => + !Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined); + const hasNonNullValue = timeSeries.values.find(hasValue); + + newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : []; + + return newTimeSeries.values.length > 0; + }), + }; +} + +function removeTimeSeriesNoData(queries) { + return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []); +} + +// Metrics and queries are currently stored 1:1, so `queries` is an array of length one. +// We want to group queries onto a single chart by title & y-axis label. +// This function will no longer be required when metrics:queries are 1:many, +// though there is no consequence if the function stays in use. +// @param metrics [Array<Object>] +// Ex) [ +// { id: 1, title: 'title', y_label: 'MB', queries: [{ ...query1Attrs }] }, +// { id: 2, title: 'title', y_label: 'MB', queries: [{ ...query2Attrs }] }, +// { id: 3, title: 'new title', y_label: 'MB', queries: [{ ...query3Attrs }] } +// ] +// @return [Array<Object>] +// Ex) [ +// { title: 'title', y_label: 'MB', queries: [{ metricId: 1, ...query1Attrs }, +// { metricId: 2, ...query2Attrs }] }, +// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]} +// ] +function groupQueriesByChartInfo(metrics) { + const metricsByChart = metrics.reduce((accumulator, metric) => { + const { queries, ...chart } = metric; + const metricId = chart.id ? chart.id.toString() : null; + + const chartKey = `${chart.title}|${chart.y_label}`; + accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] }; + + queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs })); + + return accumulator; + }, {}); + + return Object.values(metricsByChart); +} + +export const sortMetrics = (metrics) => { + return _.chain(metrics) + .sortBy('title') + .sortBy('weight') + .value(); +} + +export const normalizeMetrics = metrics => { + const groupedMetrics = groupQueriesByChartInfo(metrics); + + return groupedMetrics.map(metric => { + const queries = metric.queries.map(query => ({ + ...query, + // custom metrics do not require a label, so we should ensure this attribute is defined + label: query.label || metric.y_label, + result: query.result.map(result => ({ + ...result, + values: result.values.map(([timestamp, value]) => [ + new Date(timestamp * 1000).toISOString(), + Number(value), + ]), + })), + })); + + return { + ...metric, + queries: removeTimeSeriesNoData(queries), + }; + }); +} |