diff options
author | Olena Horal-Koretska <ohoralkoretska@gitlab.com> | 2019-08-29 22:14:58 +0300 |
---|---|---|
committer | Olena Horal-Koretska <ohoralkoretska@gitlab.com> | 2019-08-29 22:14:58 +0300 |
commit | 20ccdb0763b536c727735ad338d112f511a41f3e (patch) | |
tree | 043636931552aa764d4a3deab17e09fe5941ad59 | |
parent | ad666a697bdd3615183d7e2224cecf85f4d6def5 (diff) |
Basic structure
14 files changed, 275 insertions, 0 deletions
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue new file mode 100644 index 00000000000..6da3729b8b6 --- /dev/null +++ b/app/assets/javascripts/contributors/components/contributors.vue @@ -0,0 +1,132 @@ +<script> +import { s__ } from '~/locale'; +import { mapGetters, mapActions, mapState } from 'vuex'; +import { engineeringNotation, sum, average } from '@gitlab/ui/utils/number_utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { GlChartLegend } from '@gitlab/ui/charts'; +import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; + +export default { + components: { + GlAreaChart, + GlLoadingIcon, + GlChartLegend, + }, + props: { + endpoint: { + type: String, + required: true, + }, + }, + data() { + return { + svgs: {}, + chart: null, + seriesInfo: [ + { + type: 'solid', + name: s__('IssuesAnalytics | Issues created'), + color: '#1F78D1', + }, + ], + }; + }, + computed: { + ...mapState('contributors', ['chartData', 'loading']), + ...mapGetters('contributors', ['hasFilters', 'appliedFilters']), + data() { + const { chartData, chartHasData } = this; + const data = []; + + if (chartHasData()) { + Object.keys(chartData).forEach(key => { + const date = new Date(key); + const label = `${getMonthNames(true)[date.getUTCMonth()]} ${date.getUTCFullYear()}`; + const val = chartData[key]; + + data.push([label, val]); + }); + } + + return data; + }, + chartLabels() { + return this.data.map(val => val[0]); + }, + chartDateRange() { + return `${this.chartLabels[0]} - ${this.chartLabels[this.chartLabels.length - 1]}`; + }, + showChart() { + return !this.loading && this.chartHasData(); + }, + chartOptions() { + return { + dataZoom: [ + { + type: 'slider', + startValue: 0, + handleIcon: this.svgs['scroll-handle'], + }, + ], + }; + }, + series() { + return this.data.map(val => val[1]); + }, + seriesAverage() { + return engineeringNotation(average(...this.series), 0); + }, + seriesTotal() { + return engineeringNotation(sum(...this.series)); + }, + }, + watch: { + appliedFilters() { + this.fetchChartData(this.endpoint); + }, + showNoDataEmptyState(showEmptyState) { + if (showEmptyState) { + this.$nextTick(() => this.filterBlockEl.classList.add('hide')); + } + }, + }, + created() { + this.setSvg('scroll-handle'); + }, + mounted() { + this.fetchChartData(this.endpoint); + }, + methods: { + ...mapActions('contributors', ['fetchChartData']), + onCreated(chart) { + this.chart = chart; + }, + chartHasData() { + if (!this.chartData) { + return false; + } + + return Object.values(this.chartData).some(val => val > 0); + }, + setSvg(name) { + getSvgIconPathContent(name) + .then(path => { + if (path) { + this.$set(this.svgs, name, `path://${path}`); + } + }) + .catch(() => {}); + }, + }, +}; +</script> +<template> + <div class="issues-analytics-wrapper"> + <!-- <div v-if="loading" class="issues-analytics-loading text-center"> + <gl-loading-icon :inline="true" :size="4"/> + </div>--> + + This is just the beginning + </div> +</template> diff --git a/app/assets/javascripts/contributors/filtered_search_issues_analytics.js b/app/assets/javascripts/contributors/filtered_search_issues_analytics.js new file mode 100644 index 00000000000..7248bf2ae21 --- /dev/null +++ b/app/assets/javascripts/contributors/filtered_search_issues_analytics.js @@ -0,0 +1,27 @@ +import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; +import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; +import { historyPushState } from '~/lib/utils/common_utils'; +import issueAnalyticsStore from './stores'; + +export default class FilteredSearchIssueAnalytics extends FilteredSearchManager { + constructor() { + super({ + page: 'issues_analytics', + isGroupDecendent: true, + stateFiltersSelector: '.issues-state-filters', + isGroup: true, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, + }); + + this.isHandledAsync = true; + } + + /** + * Updates issues analytics store and window history + * with filter path + */ + updateObject = path => { + historyPushState(path); + issueAnalyticsStore.dispatch('issueAnalytics/setFilters', path); + }; +} diff --git a/app/assets/javascripts/contributors/index.js b/app/assets/javascripts/contributors/index.js new file mode 100644 index 00000000000..d2cf2083009 --- /dev/null +++ b/app/assets/javascripts/contributors/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import ContributorsGraphs from './components/contributors.vue'; +import store from './stores'; + +export default () => { + const el = document.querySelector('.js-contributors-graph'); + + if (!el) return null; + + return new Vue({ + el, + store, + components: { + ContributorsGraphs, + }, + + render(createElement) { + return createElement('contributors-graphs', { + props: { + endpoint: el.dataset.projectGraphPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/contributors/services/contributors_service.js b/app/assets/javascripts/contributors/services/contributors_service.js new file mode 100644 index 00000000000..f5f8fcad127 --- /dev/null +++ b/app/assets/javascripts/contributors/services/contributors_service.js @@ -0,0 +1,7 @@ +import axios from '~/lib/utils/axios_utils'; + +export default { + fetchChartData(endpoint, filters) { + return axios.get(`${endpoint}${filters}`); + }, +}; diff --git a/app/assets/javascripts/contributors/stores/index.js b/app/assets/javascripts/contributors/stores/index.js new file mode 100644 index 00000000000..1fb8450098d --- /dev/null +++ b/app/assets/javascripts/contributors/stores/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import contributorsGraph from './modules/contributors'; + +Vue.use(Vuex); + +export const createStore = () => + new Vuex.Store({ + modules: { + contributors: contributorsGraph(), + }, + }); + +export default createStore(); diff --git a/app/assets/javascripts/contributors/stores/modules/contributors/actions.js b/app/assets/javascripts/contributors/stores/modules/contributors/actions.js new file mode 100644 index 00000000000..0f65f0923ee --- /dev/null +++ b/app/assets/javascripts/contributors/stores/modules/contributors/actions.js @@ -0,0 +1,26 @@ +import flash from '~/flash'; +import { __ } from '~/locale'; +import service from '../../../services/contributors_service'; +import * as types from './mutation_types'; + +export const setFilters = ({ commit }, value) => { + commit(types.SET_FILTERS, value); +}; + +export const setLoadingState = ({ commit }, value) => { + commit(types.SET_LOADING_STATE, value); +}; + +export const fetchChartData = ({ commit, dispatch, getters }, endpoint) => { + dispatch('setLoadingState', true); + + return service + .fetchChartData(endpoint, getters.appliedFilters) + .then(res => res.data) + .then(data => commit(types.SET_CHART_DATA, data)) + .then(() => dispatch('setLoadingState', false)) + .catch(() => flash(__('An error occurred while loading chart data'))); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/contributors/stores/modules/contributors/getters.js b/app/assets/javascripts/contributors/stores/modules/contributors/getters.js new file mode 100644 index 00000000000..608816e58d5 --- /dev/null +++ b/app/assets/javascripts/contributors/stores/modules/contributors/getters.js @@ -0,0 +1,2 @@ +export const hasFilters = state => Object.keys(state.filters).length > 0; +export const appliedFilters = state => state.filters; diff --git a/app/assets/javascripts/contributors/stores/modules/contributors/index.js b/app/assets/javascripts/contributors/stores/modules/contributors/index.js new file mode 100644 index 00000000000..81dab0566c1 --- /dev/null +++ b/app/assets/javascripts/contributors/stores/modules/contributors/index.js @@ -0,0 +1,12 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; +import * as getters from './getters'; + +export default () => ({ + namespaced: true, + state: state(), + mutations, + actions, + getters, +}); diff --git a/app/assets/javascripts/contributors/stores/modules/contributors/mutation_types.js b/app/assets/javascripts/contributors/stores/modules/contributors/mutation_types.js new file mode 100644 index 00000000000..7d89d99622b --- /dev/null +++ b/app/assets/javascripts/contributors/stores/modules/contributors/mutation_types.js @@ -0,0 +1,3 @@ +export const SET_FILTERS = 'SET_FILTERS'; +export const SET_CHART_DATA = 'SET_CHART_DATA'; +export const SET_LOADING_STATE = 'SET_LOADING_STATE'; diff --git a/app/assets/javascripts/contributors/stores/modules/contributors/mutations.js b/app/assets/javascripts/contributors/stores/modules/contributors/mutations.js new file mode 100644 index 00000000000..9b0f0152879 --- /dev/null +++ b/app/assets/javascripts/contributors/stores/modules/contributors/mutations.js @@ -0,0 +1,15 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_LOADING_STATE](state, value) { + state.loading = value; + }, + [types.SET_CHART_DATA](state, chartData) { + Object.assign(state, { + chartData, + }); + }, + [types.SET_FILTERS](state, value) { + state.filters = value; + }, +}; diff --git a/app/assets/javascripts/contributors/stores/modules/contributors/state.js b/app/assets/javascripts/contributors/stores/modules/contributors/state.js new file mode 100644 index 00000000000..1fa6e91b5d5 --- /dev/null +++ b/app/assets/javascripts/contributors/stores/modules/contributors/state.js @@ -0,0 +1,5 @@ +export default () => ({ + loading: false, + filters: '', + chartData: null, +}); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 314519ee442..ca7db55cea1 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils'; document.addEventListener('DOMContentLoaded', () => { + debugger; const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); const barChart = (selector, data) => { diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index f79c386b59e..81e634645ad 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/index.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -3,8 +3,10 @@ import flash from '~/flash'; import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import ContributorsStatGraph from './stat_graph_contributors'; +import initContributorsGraphs from '~/contributors'; document.addEventListener('DOMContentLoaded', () => { + initContributorsGraphs(); const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; axios @@ -22,4 +24,6 @@ document.addEventListener('DOMContentLoaded', () => { $('.loading-graph').hide(); }) .catch(() => flash(__('Error fetching contributors data.'))); + + }); diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 4b2417ff43b..82c6b751fa2 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,6 +1,8 @@ - @no_container = true - page_title _('Contributors') +.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } + .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .sub-header-block .tree-ref-holder.inline.vertical-align-middle |