Read about our upcoming Code of Conduct on this issue

dashboard.vue 14.6 KB
Newer Older
1
<script>
2
3
4
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import VueDraggable from 'vuedraggable';
5
6
7
8
import {
  GlButton,
  GlDropdown,
  GlDropdownItem,
9
  GlFormGroup,
10
11
12
13
  GlModal,
  GlModalDirective,
  GlTooltipDirective,
} from '@gitlab/ui';
14
import { s__ } from '~/locale';
15
import createFlash from '~/flash';
16
import Icon from '~/vue_shared/components/icon.vue';
17
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
18
import invalidUrl from '~/lib/utils/invalid_url';
19
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
20
import DateTimePicker from './date_time_picker/date_time_picker.vue';
21
import MonitorTimeSeriesChart from './charts/time_series.vue';
22
import MonitorSingleStatChart from './charts/single_stat.vue';
Mike Greiling's avatar
Mike Greiling committed
23
24
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
25
import { getTimeDiff, isValidDate } from '../utils';
26

Mike Greiling's avatar
Mike Greiling committed
27
28
export default {
  components: {
29
    VueDraggable,
30
    MonitorTimeSeriesChart,
31
32
    MonitorSingleStatChart,
    PanelType,
Mike Greiling's avatar
Mike Greiling committed
33
34
    GraphGroup,
    EmptyState,
35
    Icon,
Adriel Santiago's avatar
Adriel Santiago committed
36
    GlButton,
37
38
    GlDropdown,
    GlDropdownItem,
39
    GlFormGroup,
40
    GlModal,
41
    DateTimePicker,
42
43
  },
  directives: {
44
45
    GlModal: GlModalDirective,
    GlTooltip: GlTooltipDirective,
Mike Greiling's avatar
Mike Greiling committed
46
47
  },
  props: {
48
    externalDashboardUrl: {
Adriel Santiago's avatar
Adriel Santiago committed
49
50
51
52
      type: String,
      required: false,
      default: '',
    },
Mike Greiling's avatar
Mike Greiling committed
53
54
55
56
    hasMetrics: {
      type: Boolean,
      required: false,
      default: true,
Filipa Lacerda's avatar
Filipa Lacerda committed
57
    },
Mike Greiling's avatar
Mike Greiling committed
58
59
60
61
    showPanels: {
      type: Boolean,
      required: false,
      default: true,
62
    },
Mike Greiling's avatar
Mike Greiling committed
63
64
65
    documentationPath: {
      type: String,
      required: true,
66
    },
Mike Greiling's avatar
Mike Greiling committed
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    settingsPath: {
      type: String,
      required: true,
    },
    clustersPath: {
      type: String,
      required: true,
    },
    tagsPath: {
      type: String,
      required: true,
    },
    projectPath: {
      type: String,
      required: true,
    },
    metricsEndpoint: {
      type: String,
      required: true,
    },
87
    deploymentsEndpoint: {
Mike Greiling's avatar
Mike Greiling committed
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
      type: String,
      required: false,
      default: null,
    },
    emptyGettingStartedSvgPath: {
      type: String,
      required: true,
    },
    emptyLoadingSvgPath: {
      type: String,
      required: true,
    },
    emptyNoDataSvgPath: {
      type: String,
      required: true,
    },
    emptyUnableToConnectSvgPath: {
      type: String,
      required: true,
    },
108
109
110
111
    environmentsEndpoint: {
      type: String,
      required: true,
    },
112
113
114
115
    currentEnvironmentName: {
      type: String,
      required: true,
    },
116
117
118
119
120
121
122
    customMetricsAvailable: {
      type: Boolean,
      required: false,
      default: false,
    },
    customMetricsPath: {
      type: String,
123
124
      required: false,
      default: invalidUrl,
125
126
127
    },
    validateQueryPath: {
      type: String,
128
129
      required: false,
      default: invalidUrl,
130
    },
131
132
133
134
135
    dashboardEndpoint: {
      type: String,
      required: false,
      default: invalidUrl,
    },
136
137
138
139
140
    currentDashboard: {
      type: String,
      required: false,
      default: '',
    },
141
142
143
144
145
    smallEmptyState: {
      type: Boolean,
      required: false,
      default: false,
    },
146
147
148
149
150
151
152
153
154
155
    alertsEndpoint: {
      type: String,
      required: false,
      default: null,
    },
    prometheusAlertsAvailable: {
      type: Boolean,
      required: false,
      default: false,
    },
156
157
158
159
160
    rearrangePanelsAvailable: {
      type: Boolean,
      required: false,
      default: false,
    },
Mike Greiling's avatar
Mike Greiling committed
161
162
163
164
  },
  data() {
    return {
      state: 'gettingStarted',
165
      formIsValid: null,
166
      selectedTimeWindow: {},
167
      isRearrangingPanels: false,
168
      hasValidDates: true,
Mike Greiling's avatar
Mike Greiling committed
169
170
    };
  },
171
172
173
174
  computed: {
    canAddMetrics() {
      return this.customMetricsAvailable && this.customMetricsPath.length;
    },
175
    ...mapState('monitoringDashboard', [
176
      'dashboard',
177
178
179
180
      'emptyState',
      'showEmptyState',
      'environments',
      'deploymentData',
181
182
      'metricsWithData',
      'useDashboardEndpoint',
183
      'allDashboards',
184
      'additionalPanelTypesEnabled',
185
    ]),
186
    firstDashboard() {
187
188
189
      return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
        ? this.allDashboards[0]
        : {};
190
    },
191
192
193
    selectedDashboard() {
      return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
    },
194
    selectedDashboardText() {
195
      return this.selectedDashboard.display_name;
196
    },
197
198
199
    showRearrangePanelsBtn() {
      return !this.showEmptyState && this.rearrangePanelsAvailable;
    },
200
201
202
    addingMetricsAvailable() {
      return IS_EE && this.canAddMetrics && !this.showEmptyState;
    },
203
204
205
206
207
208
209
210
    hasHeaderButtons() {
      return (
        this.addingMetricsAvailable ||
        this.showRearrangePanelsBtn ||
        this.selectedDashboard.can_edit ||
        this.externalDashboardUrl.length
      );
    },
211
  },
Mike Greiling's avatar
Mike Greiling committed
212
  created() {
213
    this.setEndpoints({
Mike Greiling's avatar
Mike Greiling committed
214
      metricsEndpoint: this.metricsEndpoint,
215
      environmentsEndpoint: this.environmentsEndpoint,
216
      deploymentsEndpoint: this.deploymentsEndpoint,
217
      dashboardEndpoint: this.dashboardEndpoint,
218
      currentDashboard: this.currentDashboard,
219
      projectPath: this.projectPath,
Mike Greiling's avatar
Mike Greiling committed
220
221
222
223
    });
  },
  mounted() {
    if (!this.hasMetrics) {
224
      this.setGettingStartedEmptyState();
Mike Greiling's avatar
Mike Greiling committed
225
    } else {
Simon Knox's avatar
Simon Knox committed
226
227
228
229
230
231
232
233
234
      const defaultRange = getTimeDiff();
      const start = getParameterValues('start')[0] || defaultRange.start;
      const end = getParameterValues('end')[0] || defaultRange.end;

      const range = {
        start,
        end,
      };

235
      this.selectedTimeWindow = range;
Simon Knox's avatar
Simon Knox committed
236

237
      if (!isValidDate(start) || !isValidDate(end)) {
238
        this.hasValidDates = false;
239
240
        this.showInvalidDateError();
      } else {
241
        this.hasValidDates = true;
242
243
        this.fetchData(range);
      }
Mike Greiling's avatar
Mike Greiling committed
244
245
246
    }
  },
  methods: {
247
248
249
250
    ...mapActions('monitoringDashboard', [
      'fetchData',
      'setGettingStartedEmptyState',
      'setEndpoints',
251
      'setPanelGroupMetrics',
252
    ]),
253
    updateMetrics(key, panels) {
254
      this.setPanelGroupMetrics({
255
        panels,
256
257
258
259
260
261
262
263
        key,
      });
    },
    removeMetric(key, metrics, graphIndex) {
      this.setPanelGroupMetrics({
        metrics: metrics.filter((v, i) => i !== graphIndex),
        key,
      });
264
    },
265
266
267
268
269
    removeGraph(metrics, graphIndex) {
      // At present graphs will not be removed, they should removed using the vuex store
      // See https://gitlab.com/gitlab-org/gitlab/issues/27835
      metrics.splice(graphIndex, 1);
    },
270
271
272
    showInvalidDateError() {
      createFlash(s__('Metrics|Link contains an invalid time window.'));
    },
273
274
    generateLink(group, title, yLabel) {
      const dashboard = this.currentDashboard || this.firstDashboard.path;
275
      const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
276
277
      return mergeUrlParams(params, window.location.href);
    },
278
279
280
    hideAddMetricModal() {
      this.$refs.addMetricModal.hide();
    },
281
282
283
    toggleRearrangingPanels() {
      this.isRearrangingPanels = !this.isRearrangingPanels;
    },
284
285
286
287
288
289
    setFormValidity(isValid) {
      this.formIsValid = isValid;
    },
    submitCustomMetricsForm() {
      this.$refs.customMetricsForm.submit();
    },
290
291
292
293
294
    chartsWithData(panels) {
      return panels.filter(panel =>
        panel.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
      );
    },
Jose Ivan Vargas's avatar
Jose Ivan Vargas committed
295
    groupHasData(group) {
296
      return this.chartsWithData(group.panels).length > 0;
Jose Ivan Vargas's avatar
Jose Ivan Vargas committed
297
    },
298
299
300
    onDateTimePickerApply(timeWindowUrlParams) {
      return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
    },
Mike Greiling's avatar
Mike Greiling committed
301
  },
302
303
304
305
  addMetric: {
    title: s__('Metrics|Add metric'),
    modalId: 'add-metric',
  },
Mike Greiling's avatar
Mike Greiling committed
306
};
307
</script>
308

309
<template>
310
  <div class="prometheus-graphs">
311
    <div class="prometheus-graphs-header gl-p-3 pb-0 border-bottom bg-gray-light">
312
313
314
315
316
317
      <div class="row">
        <template v-if="environmentsEndpoint">
          <gl-form-group
            :label="__('Dashboard')"
            label-size="sm"
            label-for="monitor-dashboards-dropdown"
318
            class="col-sm-12 col-md-6 col-lg-2"
319
          >
320
321
322
323
324
            <gl-dropdown
              id="monitor-dashboards-dropdown"
              class="mb-0 d-flex js-dashboards-dropdown"
              toggle-class="dropdown-menu-toggle"
              :text="selectedDashboardText"
325
            >
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
              <gl-dropdown-item
                v-for="dashboard in allDashboards"
                :key="dashboard.path"
                :active="dashboard.path === currentDashboard"
                active-class="is-active"
                :href="`?dashboard=${dashboard.path}`"
                >{{ dashboard.display_name || dashboard.path }}</gl-dropdown-item
              >
            </gl-dropdown>
          </gl-form-group>

          <gl-form-group
            :label="s__('Metrics|Environment')"
            label-size="sm"
            label-for="monitor-environments-dropdown"
341
            class="col-sm-6 col-md-6 col-lg-2"
342
          >
343
344
345
346
347
348
            <gl-dropdown
              id="monitor-environments-dropdown"
              class="mb-0 d-flex js-environments-dropdown"
              toggle-class="dropdown-menu-toggle"
              :text="currentEnvironmentName"
              :disabled="environments.length === 0"
349
            >
350
351
352
353
354
355
356
357
358
359
360
361
              <gl-dropdown-item
                v-for="environment in environments"
                :key="environment.id"
                :active="environment.name === currentEnvironmentName"
                active-class="is-active"
                :href="environment.metrics_path"
                >{{ environment.name }}</gl-dropdown-item
              >
            </gl-dropdown>
          </gl-form-group>

          <gl-form-group
362
            v-if="hasValidDates"
363
364
365
            :label="s__('Metrics|Show last')"
            label-size="sm"
            label-for="monitor-time-window-dropdown"
366
            class="col-sm-6 col-md-6 col-lg-4"
367
          >
368
369
370
371
            <date-time-picker
              :selected-time-window="selectedTimeWindow"
              @onApply="onDateTimePickerApply"
            />
372
373
374
375
          </gl-form-group>
        </template>

        <gl-form-group
376
          v-if="hasHeaderButtons"
377
          label-for="prometheus-graphs-dropdown-buttons"
378
          class="dropdown-buttons col-md d-md-flex col-lg d-lg-flex align-items-end"
379
        >
380
          <div id="prometheus-graphs-dropdown-buttons">
381
382
383
384
385
386
387
388
389
            <gl-button
              v-if="showRearrangePanelsBtn"
              :pressed="isRearrangingPanels"
              variant="default"
              class="mr-2 mt-1 js-rearrange-button"
              @click="toggleRearrangingPanels"
            >
              {{ __('Arrange charts') }}
            </gl-button>
390
391
392
            <gl-button
              v-if="addingMetricsAvailable"
              v-gl-modal="$options.addMetric.modalId"
393
394
              variant="outline-success"
              class="mr-2 mt-1 js-add-metric-button"
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
            >
              {{ $options.addMetric.title }}
            </gl-button>
            <gl-modal
              v-if="addingMetricsAvailable"
              ref="addMetricModal"
              :modal-id="$options.addMetric.modalId"
              :title="$options.addMetric.title"
            >
              <form ref="customMetricsForm" :action="customMetricsPath" method="post">
                <custom-metrics-form-fields
                  :validate-query-path="validateQueryPath"
                  form-operation="post"
                  @formValidation="setFormValidity"
                />
              </form>
              <div slot="modal-footer">
                <gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button>
                <gl-button
                  :disabled="!formIsValid"
                  variant="success"
                  @click="submitCustomMetricsForm"
                >
                  {{ __('Save changes') }}
                </gl-button>
              </div>
            </gl-modal>

423
424
425
426
427
428
429
430
            <gl-button
              v-if="selectedDashboard.can_edit"
              class="mt-1 js-edit-link"
              :href="selectedDashboard.project_blob_path"
            >
              {{ __('Edit dashboard') }}
            </gl-button>

431
432
433
434
435
436
437
438
439
440
441
442
443
            <gl-button
              v-if="externalDashboardUrl.length"
              class="mt-1 js-external-dashboard-link"
              variant="primary"
              :href="externalDashboardUrl"
              target="_blank"
              rel="noopener noreferrer"
            >
              {{ __('View full dashboard') }}
              <icon name="external-link" />
            </gl-button>
          </div>
        </gl-form-group>
444
      </div>
Jose's avatar
Jose committed
445
    </div>
446

447
448
    <div v-if="!showEmptyState">
      <graph-group
449
        v-for="(groupData, index) in dashboard.panel_groups"
Tristan Read's avatar
Tristan Read committed
450
        :key="`${groupData.group}.${groupData.priority}`"
451
452
        :name="groupData.group"
        :show-panels="showPanels"
Jose Ivan Vargas's avatar
Jose Ivan Vargas committed
453
        :collapse-group="groupHasData(groupData)"
454
      >
455
        <vue-draggable
456
          :value="groupData.panels"
457
458
459
460
461
462
          group="metrics-dashboard"
          :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
          :disabled="!isRearrangingPanels"
          @input="updateMetrics(groupData.key, $event)"
        >
          <div
463
            v-for="(graphData, graphIndex) in groupData.panels"
464
465
466
            :key="`panel-type-${graphIndex}`"
            class="col-12 col-lg-6 px-2 mb-2 draggable"
            :class="{ 'draggable-enabled': isRearrangingPanels }"
467
          >
468
469
470
471
            <div class="position-relative draggable-panel js-draggable-panel">
              <div
                v-if="isRearrangingPanels"
                class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
472
                @click="removeGraph(groupData.panels, graphIndex)"
473
474
475
476
              >
                <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
                  ><icon name="close"
                /></a>
477
              </div>
478
479
480
481

              <panel-type
                :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
                :graph-data="graphData"
482
                :alerts-endpoint="alertsEndpoint"
483
484
                :prometheus-alerts-available="prometheusAlertsAvailable"
                :index="`${index}-${graphIndex}`"
485
486
              />
            </div>
487
488
          </div>
        </vue-draggable>
489
490
491
492
493
494
495
496
497
498
499
500
      </graph-group>
    </div>
    <empty-state
      v-else
      :selected-state="emptyState"
      :documentation-path="documentationPath"
      :settings-path="settingsPath"
      :clusters-path="clustersPath"
      :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
      :empty-loading-svg-path="emptyLoadingSvgPath"
      :empty-no-data-svg-path="emptyNoDataSvgPath"
      :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
501
      :compact="smallEmptyState"
502
    />
503
504
  </div>
</template>