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

100% Statements 25/25
100% Branches 8/8
100% Functions 14/14
100% Lines 25/25

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  2x 2x 2x                                                   21x                                                         22x     25x             21x 21x 42x                                   1x     6x         1x 1x     58x                               2x 2x 1x 1x 1x 1x       2x 2x 2x 2x             150x                                                                            
<script>
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
 
export default {
  name: 'GraphViewSelector',
  components: {
    GlAlert,
    GlButton,
    GlButtonGroup,
    GlLoadingIcon,
    GlToggle,
  },
  props: {
    showLinks: {
      type: Boolean,
      required: true,
    },
    tipPreviouslyDismissed: {
      type: Boolean,
      required: true,
    },
    type: {
      type: String,
      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'),
  },
  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);
      });
    },
  },
};
</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>
 
      <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>
  </div>
</template>