All files / app/assets/javascripts/pipelines/components/graph graph_view_selector.vue

100% Statements 1/1
100% Branches 2/2
100% Functions 1/1
100% Lines 1/1

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209                                                                                                                                                                                                                                                                                                                              179x                                                                                                  
<script>
import {
  GlAlert,
  GlButton,
  GlButtonGroup,
  GlLoadingIcon,
  GlToggle,
  GlModalDirective,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import PerformanceInsightsModal from '../performance_insights_modal.vue';
import { performanceModalId } from '../../constants';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
 
export default {
  name: 'GraphViewSelector',
  performanceModalId,
  components: {
    GlAlert,
    GlButton,
    GlButtonGroup,
    GlLoadingIcon,
    GlToggle,
    PerformanceInsightsModal,
  },
  directives: {
    GlModal: GlModalDirective,
  },
  mixins: [Tracking.mixin()],
  props: {
    showLinks: {
      type: Boolean,
      required: true,
    },
    tipPreviouslyDismissed: {
      type: Boolean,
      required: true,
    },
    type: {
      type: String,
      required: true,
    },
    isPipelineComplete: {
      type: Boolean,
      required: true,
    },
  },
  data() {
    return {
      hoverTipDismissed: false,
      isToggleLoading: false,
      isSwitcherLoading: false,
      segmentSelectedType: this.type,
      showLinksActive: false,
    };
  },
  i18n: {
    hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'),
    linksLabelText: s__('GraphViewType|Show dependencies'),
    viewLabelText: __('Group jobs by'),
    performanceBtnText: __('Performance insights'),
  },
  views: {
    [STAGE_VIEW]: {
      type: STAGE_VIEW,
      text: {
        primary: s__('GraphViewType|Stage'),
      },
    },
    [LAYER_VIEW]: {
      type: LAYER_VIEW,
      text: {
        primary: s__('GraphViewType|Job dependencies'),
      },
    },
  },
  computed: {
    showLinksToggle() {
      return this.segmentSelectedType === LAYER_VIEW;
    },
    showTip() {
      return (
        this.showLinks &&
        this.showLinksActive &&
        !this.tipPreviouslyDismissed &&
        !this.hoverTipDismissed
      );
    },
    viewTypesList() {
      return Object.keys(this.$options.views).map((key) => {
        return {
          value: key,
          text: this.$options.views[key].text.primary,
        };
      });
    },
  },
  watch: {
    /*
      How does this reset the loading? As we note in the methods comment below,
      the loader is set to on before the update work is undertaken (in the parent).
      Once the work is complete, one of these values will change, since that's the
      point of the work. When that happens, the related value will update and we are done.
 
      The bonus for this approach is that it works the same whichever "direction"
      the work goes in.
    */
    showLinks() {
      this.isToggleLoading = false;
    },
    type() {
      this.isSwitcherLoading = false;
    },
  },
  methods: {
    dismissTip() {
      this.hoverTipDismissed = true;
      this.$emit('dismissHoverTip');
    },
    isCurrentType(type) {
      return this.segmentSelectedType === type;
    },
    /*
      In both toggle methods, we use setTimeout so that the loading indicator displays,
      then the work is done to update the DOM. The process is:
        → user clicks
        → call stack: set loading to true
        → render: the loading icon appears on the screen
        → callback queue: now do the work to calculate the new view / links
          (note: this work is done in the parent after the event is emitted)
 
      setTimeout is how we move the work to the callback queue.
      We can't use nextTick because that is called before the render loop.
 
     See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
    */
    setViewType(type) {
      if (!this.isCurrentType(type)) {
        this.isSwitcherLoading = true;
        this.segmentSelectedType = type;
        setTimeout(() => {
          this.$emit('updateViewType', type);
        });
      }
    },
    toggleShowLinksActive(val) {
      this.isToggleLoading = true;
      setTimeout(() => {
        this.$emit('updateShowLinksState', val);
      });
    },
    trackInsightsClick() {
      this.track('click_insights_button', { label: 'performance_insights' });
    },
  },
};
</script>
 
<template>
  <div>
    <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
      <gl-loading-icon
        v-if="isSwitcherLoading"
        data-testid="switcher-loading-state"
        class="gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
        size="lg"
      />
      <span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
      <gl-button-group class="gl-mx-4">
        <gl-button
          v-for="viewType in viewTypesList"
          :key="viewType.value"
          :selected="isCurrentType(viewType.value)"
          @click="setViewType(viewType.value)"
        >
          {{ viewType.text }}
        </gl-button>
      </gl-button-group>
 
      <gl-button
        v-if="isPipelineComplete"
        v-gl-modal="$options.performanceModalId"
        data-testid="pipeline-insights-btn"
        @click="trackInsightsClick"
      >
        {{ $options.i18n.performanceBtnText }}
      </gl-button>
 
      <div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
        <gl-toggle
          v-model="showLinksActive"
          data-testid="show-links-toggle"
          class="gl-mx-4"
          :label="$options.i18n.linksLabelText"
          :is-loading="isToggleLoading"
          label-position="left"
          @change="toggleShowLinksActive"
        />
      </div>
    </div>
    <gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip">
      {{ $options.i18n.hoverTipText }}
    </gl-alert>
 
    <performance-insights-modal />
  </div>
</template>