Commit 82b3fcf6 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '326480-devops-adoption-add-devsecops-tabs-to-table-2' into 'master'

Resolve "[DevOps Adoption] Refactor table into section component"

See merge request gitlab-org/gitlab!61298
parents 47276482 0dfb0978
<script> <script>
import { import { GlAlert } from '@gitlab/ui';
GlLoadingIcon,
GlButton,
GlSprintf,
GlAlert,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import { import {
...@@ -15,7 +8,6 @@ import { ...@@ -15,7 +8,6 @@ import {
MAX_REQUEST_COUNT, MAX_REQUEST_COUNT,
MAX_SEGMENTS, MAX_SEGMENTS,
DATE_TIME_FORMAT, DATE_TIME_FORMAT,
DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
DEFAULT_POLLING_INTERVAL, DEFAULT_POLLING_INTERVAL,
DEVOPS_ADOPTION_GROUP_LEVEL_LABEL, DEVOPS_ADOPTION_GROUP_LEVEL_LABEL,
DEVOPS_ADOPTION_TABLE_CONFIGURATION, DEVOPS_ADOPTION_TABLE_CONFIGURATION,
...@@ -25,24 +17,15 @@ import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segm ...@@ -25,24 +17,15 @@ import devopsAdoptionSegmentsQuery from '../graphql/queries/devops_adoption_segm
import getGroupsQuery from '../graphql/queries/get_groups.query.graphql'; import getGroupsQuery from '../graphql/queries/get_groups.query.graphql';
import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates'; import { addSegmentsToCache, deleteSegmentsFromCache } from '../utils/cache_updates';
import { shouldPollTableData } from '../utils/helpers'; import { shouldPollTableData } from '../utils/helpers';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue'; import DevopsAdoptionSection from './devops_adoption_section.vue';
import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue'; import DevopsAdoptionSegmentModal from './devops_adoption_segment_modal.vue';
import DevopsAdoptionTable from './devops_adoption_table.vue';
export default { export default {
name: 'DevopsAdoptionApp', name: 'DevopsAdoptionApp',
components: { components: {
GlAlert, GlAlert,
GlLoadingIcon, DevopsAdoptionSection,
DevopsAdoptionEmptyState,
DevopsAdoptionSegmentModal, DevopsAdoptionSegmentModal,
DevopsAdoptionTable,
GlButton,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
}, },
inject: { inject: {
isGroup: { isGroup: {
...@@ -57,14 +40,12 @@ export default { ...@@ -57,14 +40,12 @@ export default {
...DEVOPS_ADOPTION_STRINGS.app, ...DEVOPS_ADOPTION_STRINGS.app,
}, },
maxSegments: MAX_SEGMENTS, maxSegments: MAX_SEGMENTS,
devopsSegmentModalId: DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION, devopsAdoptionTableConfiguration: DEVOPS_ADOPTION_TABLE_CONFIGURATION,
data() { data() {
return { return {
isLoadingGroups: false, isLoadingGroups: false,
isLoadingEnableGroup: false, isLoadingEnableGroup: false,
requestCount: 0, requestCount: 0,
selectedSegment: null,
openModal: false, openModal: false,
errors: { errors: {
[DEVOPS_ADOPTION_ERROR_KEYS.groups]: false, [DEVOPS_ADOPTION_ERROR_KEYS.groups]: false,
...@@ -129,20 +110,17 @@ export default { ...@@ -129,20 +110,17 @@ export default {
this.$apollo.queries.devopsAdoptionSegments.loading this.$apollo.queries.devopsAdoptionSegments.loading
); );
}, },
modalKey() {
return this.selectedSegment?.id;
},
segmentLimitReached() { segmentLimitReached() {
return this.devopsAdoptionSegments.nodes?.length > this.$options.maxSegments; return this.devopsAdoptionSegments?.nodes?.length > this.$options.maxSegments;
},
addSegmentButtonTooltipText() {
return this.segmentLimitReached ? this.$options.i18n.tableHeader.buttonTooltip : false;
}, },
editGroupsButtonLabel() { editGroupsButtonLabel() {
return this.isGroup return this.isGroup
? this.$options.i18n.groupLevelLabel ? this.$options.i18n.groupLevelLabel
: this.$options.i18n.tableHeader.button; : this.$options.i18n.tableHeader.button;
}, },
canRenderModal() {
return this.hasGroupData && !this.isLoading;
},
}, },
created() { created() {
this.fetchGroups(); this.fetchGroups();
...@@ -151,6 +129,9 @@ export default { ...@@ -151,6 +129,9 @@ export default {
clearInterval(this.pollingTableData); clearInterval(this.pollingTableData);
}, },
methods: { methods: {
openAddRemoveModal() {
this.$refs.addRemoveModal.openModal();
},
enableGroup() { enableGroup() {
this.isLoadingEnableGroup = true; this.isLoadingEnableGroup = true;
...@@ -228,12 +209,6 @@ export default { ...@@ -228,12 +209,6 @@ export default {
}) })
.catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error)); .catch((error) => this.handleError(DEVOPS_ADOPTION_ERROR_KEYS.groups, error));
}, },
setSelectedSegment(segment) {
this.selectedSegment = segment;
},
clearSelectedSegment() {
this.selectedSegment = null;
},
addSegmentsToCache(segments) { addSegmentsToCache(segments) {
const { cache } = this.$apollo.getClient(); const { cache } = this.$apollo.getClient();
...@@ -255,53 +230,28 @@ export default { ...@@ -255,53 +230,28 @@ export default {
</gl-alert> </gl-alert>
</template> </template>
</div> </div>
<gl-loading-icon v-else-if="isLoading" size="md" class="gl-my-5" />
<div v-else> <div v-else>
<devops-adoption-segment-modal <devops-adoption-segment-modal
v-if="hasGroupData" v-if="canRenderModal"
:key="modalKey" ref="addRemoveModal"
:groups="groups.nodes" :groups="groups.nodes"
:enabled-groups="devopsAdoptionSegments.nodes" :enabled-groups="devopsAdoptionSegments.nodes"
@segmentsAdded="addSegmentsToCache" @segmentsAdded="addSegmentsToCache"
@segmentsRemoved="deleteSegmentsFromCache" @segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState" @trackModalOpenState="trackModalOpenState"
/> />
<div v-if="hasSegmentsData" class="gl-mt-3"> <devops-adoption-section
<div :is-loading="isLoading"
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-my-3" :has-segments-data="hasSegmentsData"
data-testid="tableHeader" :timestamp="timestamp"
> :has-group-data="hasGroupData"
<span class="gl-text-gray-400"> :segment-limit-reached="segmentLimitReached"
<gl-sprintf :message="$options.i18n.tableHeader.text"> :edit-groups-button-label="editGroupsButtonLabel"
<template #timestamp>{{ timestamp }}</template> :cols="$options.devopsAdoptionTableConfiguration[0].cols"
</gl-sprintf> :segments="devopsAdoptionSegments"
</span> @segmentsRemoved="deleteSegmentsFromCache"
<span @openAddRemoveModal="openAddRemoveModal"
v-if="hasGroupData"
v-gl-tooltip.hover="addSegmentButtonTooltipText"
data-testid="segmentButtonWrapper"
>
<gl-button
v-gl-modal="$options.devopsSegmentModalId"
:disabled="segmentLimitReached"
@click="clearSelectedSegment"
>{{ editGroupsButtonLabel }}</gl-button
></span
>
</div>
<devops-adoption-table
:cols="$options.devopsAdoptionTableConfiguration[0].cols"
:segments="devopsAdoptionSegments.nodes"
:selected-segment="selectedSegment"
@set-selected-segment="setSelectedSegment"
@segmentsRemoved="deleteSegmentsFromCache"
@trackModalOpenState="trackModalOpenState"
/>
</div>
<devops-adoption-empty-state
v-else
:has-groups-data="hasGroupData"
@clear-selected-segment="clearSelectedSegment"
/> />
</div> </div>
</template> </template>
<script>
import { GlLoadingIcon, GlTooltipDirective, GlButton, GlSprintf } from '@gitlab/ui';
import { TABLE_HEADER_TEXT, ADD_REMOVE_BUTTON_TOOLTIP } from '../constants';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
import DevopsAdoptionTable from './devops_adoption_table.vue';
export default {
components: {
DevopsAdoptionTable,
GlLoadingIcon,
GlButton,
GlSprintf,
DevopsAdoptionEmptyState,
},
i18n: {
tableHeaderText: TABLE_HEADER_TEXT,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
isLoading: {
type: Boolean,
required: true,
},
hasSegmentsData: {
type: Boolean,
required: true,
},
timestamp: {
type: String,
required: true,
},
hasGroupData: {
type: Boolean,
required: true,
},
segmentLimitReached: {
type: Boolean,
required: true,
},
editGroupsButtonLabel: {
type: String,
required: true,
},
cols: {
type: Array,
required: true,
},
segments: {
type: Object,
required: false,
default: () => {},
},
},
computed: {
addSegmentButtonTooltipText() {
return this.segmentLimitReached ? ADD_REMOVE_BUTTON_TOOLTIP : false;
},
},
};
</script>
<template>
<gl-loading-icon v-if="isLoading" size="md" class="gl-my-5" />
<div v-else-if="hasSegmentsData" class="gl-mt-3">
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-my-3"
data-testid="tableHeader"
>
<span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.tableHeaderText">
<template #timestamp>{{ timestamp }}</template>
</gl-sprintf>
</span>
<span
v-if="hasGroupData"
v-gl-tooltip.hover="addSegmentButtonTooltipText"
data-testid="segmentButtonWrapper"
>
<gl-button :disabled="segmentLimitReached" @click="$emit('openAddRemoveModal')">{{
editGroupsButtonLabel
}}</gl-button></span
>
</div>
<devops-adoption-table
:cols="cols"
:segments="segments.nodes"
@segmentsRemoved="$emit('segmentsRemoved', $event)"
@trackModalOpenState="$emit('trackModalOpenState', $event)"
/>
</div>
<devops-adoption-empty-state v-else :has-groups-data="hasGroupData" />
</template>
...@@ -108,6 +108,16 @@ export default { ...@@ -108,6 +108,16 @@ export default {
); );
}, },
}, },
watch: {
enabledGroups(newValues) {
if (!this.loading) {
this.checkboxValuesFromEnabledGroups = newValues.map((group) =>
getIdFromGraphQLId(group.namespace.id),
);
this.checkboxValues = this.checkboxValuesFromEnabledGroups;
}
},
},
methods: { methods: {
async saveChanges() { async saveChanges() {
await this.deleteMissingGroups(); await this.deleteMissingGroups();
...@@ -201,6 +211,9 @@ export default { ...@@ -201,6 +211,9 @@ export default {
clearErrors() { clearErrors() {
this.errors = []; this.errors = [];
}, },
openModal() {
this.$refs.modal.show();
},
closeModal() { closeModal() {
this.$refs.modal.hide(); this.$refs.modal.hide();
}, },
......
...@@ -81,11 +81,6 @@ export default { ...@@ -81,11 +81,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
selectedSegment: {
type: Object,
required: false,
default: null,
},
cols: { cols: {
type: Array, type: Array,
required: true, required: true,
...@@ -95,6 +90,7 @@ export default { ...@@ -95,6 +90,7 @@ export default {
return { return {
sortBy: NAME_HEADER, sortBy: NAME_HEADER,
sortDesc: false, sortDesc: false,
selectedSegment: null,
}; };
}, },
computed: { computed: {
...@@ -120,7 +116,7 @@ export default { ...@@ -120,7 +116,7 @@ export default {
}, },
methods: { methods: {
setSelectedSegment(segment) { setSelectedSegment(segment) {
this.$emit('set-selected-segment', segment); this.selectedSegment = segment;
}, },
headerSlotName(key) { headerSlotName(key) {
return `head(${key})`; return `head(${key})`;
......
...@@ -18,6 +18,17 @@ export const DEVOPS_ADOPTION_ERROR_KEYS = { ...@@ -18,6 +18,17 @@ export const DEVOPS_ADOPTION_ERROR_KEYS = {
addSegment: 'addSegmentsError', addSegment: 'addSegmentsError',
}; };
export const TABLE_HEADER_TEXT = s__(
'DevopsAdoption|Feature adoption is based on usage in the current calendar month. Last updated: %{timestamp}.',
);
export const ADD_REMOVE_BUTTON_TOOLTIP = sprintf(
s__('DevopsAdoption|Maximum %{maxSegments} groups allowed'),
{
maxSegments: MAX_SEGMENTS,
},
);
export const DEVOPS_ADOPTION_GROUP_LEVEL_LABEL = s__('DevopsAdoption|Add/remove sub-groups'); export const DEVOPS_ADOPTION_GROUP_LEVEL_LABEL = s__('DevopsAdoption|Add/remove sub-groups');
export const DEVOPS_ADOPTION_TABLE_REMOVE_BUTTON_DISABLED = s__( export const DEVOPS_ADOPTION_TABLE_REMOVE_BUTTON_DISABLED = s__(
...@@ -36,13 +47,7 @@ export const DEVOPS_ADOPTION_STRINGS = { ...@@ -36,13 +47,7 @@ export const DEVOPS_ADOPTION_STRINGS = {
'DevopsAdoption|There was an error enabling the current group. Please refresh the page.', 'DevopsAdoption|There was an error enabling the current group. Please refresh the page.',
), ),
tableHeader: { tableHeader: {
text: s__(
'DevopsAdoption|Feature adoption is based on usage in the current calendar month. Last updated: %{timestamp}.',
),
button: s__('DevopsAdoption|Add/remove groups'), button: s__('DevopsAdoption|Add/remove groups'),
buttonTooltip: sprintf(s__('DevopsAdoption|Maximum %{maxSegments} groups allowed'), {
maxSegments: MAX_SEGMENTS,
}),
}, },
}, },
emptyState: { emptyState: {
......
import { GlLoadingIcon, GlButton, GlSprintf } from '@gitlab/ui';
import { getByText } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import DevopsAdoptionEmptyState from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_empty_state.vue';
import DevopsAdoptionSection from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_section.vue';
import DevopsAdoptionTable from 'ee/analytics/devops_report/devops_adoption/components/devops_adoption_table.vue';
import { DEVOPS_ADOPTION_TABLE_CONFIGURATION } from 'ee/analytics/devops_report/devops_adoption/constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { devopsAdoptionSegmentsData } from '../mock_data';
describe('DevopsAdoptionSection', () => {
let wrapper;
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(DevopsAdoptionSection, {
propsData: {
isLoading: false,
hasSegmentsData: true,
timestamp: '2020-10-31 23:59',
hasGroupData: true,
segmentLimitReached: false,
editGroupsButtonLabel: 'Add/Remove groups',
cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols,
segments: devopsAdoptionSegmentsData,
addSegmentButtonTooltipText: 'Maximum 30 groups allowed',
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
stubs: {
GlSprintf,
},
}),
);
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTableHeaderSection = () => wrapper.findByTestId('tableHeader');
const findTable = () => wrapper.findComponent(DevopsAdoptionTable);
const findEmptyState = () => wrapper.findComponent(DevopsAdoptionEmptyState);
const findAddEditButton = () => wrapper.findComponent(GlButton);
const findAddRemoveButtonWrapper = () => wrapper.findByTestId('segmentButtonWrapper');
describe('while loading', () => {
beforeEach(() => {
createComponent({ isLoading: true });
});
it('displays a loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not display the table header section', () => {
expect(findTableHeaderSection().exists()).toBe(false);
});
it('does not display the table', () => {
expect(findTable().exists()).toBe(false);
});
});
describe('with segment data', () => {
beforeEach(() => {
createComponent();
});
it('does not display a loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('does not display an empty state', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('displays the table header section', () => {
expect(findTableHeaderSection().exists()).toBe(true);
});
it('displays the table', () => {
expect(findTableHeaderSection().exists()).toBe(true);
});
});
describe('with no segment data', () => {
beforeEach(() => {
createComponent({ hasSegmentsData: false });
});
it('displays an empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
});
describe('table header section', () => {
it('displays the header message with timestamp', () => {
createComponent();
const text =
'Feature adoption is based on usage in the current calendar month. Last updated: 2020-10-31 23:59.';
expect(getByText(wrapper.element, text)).not.toBeNull();
});
describe('with group data', () => {
it('displays the edit groups button', () => {
createComponent();
expect(findAddEditButton().exists()).toBe(true);
});
describe('edit groups button', () => {
describe('segment limit reached', () => {
beforeEach(() => {
createComponent({ segmentLimitReached: true });
});
it('is disabled', () => {
expect(findAddEditButton().props('disabled')).toBe(true);
});
it('displays a tooltip', () => {
const tooltip = getBinding(findAddRemoveButtonWrapper().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe('Maximum 30 groups allowed');
});
});
describe('segment limit not reached', () => {
beforeEach(() => {
createComponent();
});
it('is enabled', () => {
expect(findAddEditButton().props('disabled')).toBe(false);
});
it('does not display a tooltip', () => {
const tooltip = getBinding(findAddRemoveButtonWrapper().element, 'gl-tooltip');
expect(tooltip.value).toBe(false);
});
it('emits openAddRemoveModal when clicked', () => {
expect(wrapper.emitted('openAddRemoveModal')).toBeUndefined();
findAddEditButton().vm.$emit('click');
expect(wrapper.emitted('openAddRemoveModal')).toEqual([[]]);
});
});
});
});
describe('with no group data', () => {
beforeEach(() => {
createComponent({ hasGroupData: false });
});
it('does not display the edit groups button', () => {
expect(findAddEditButton().exists()).toBe(false);
});
});
});
});
...@@ -22,7 +22,6 @@ describe('DevopsAdoptionTable', () => { ...@@ -22,7 +22,6 @@ describe('DevopsAdoptionTable', () => {
propsData: { propsData: {
cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols, cols: DEVOPS_ADOPTION_TABLE_CONFIGURATION[0].cols,
segments: devopsAdoptionSegmentsData.nodes, segments: devopsAdoptionSegmentsData.nodes,
selectedSegment: devopsAdoptionSegmentsData.nodes[0],
}, },
provide, provide,
directives: { directives: {
...@@ -37,7 +36,6 @@ describe('DevopsAdoptionTable', () => { ...@@ -37,7 +36,6 @@ describe('DevopsAdoptionTable', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.find(GlTable);
...@@ -198,6 +196,8 @@ describe('DevopsAdoptionTable', () => { ...@@ -198,6 +196,8 @@ describe('DevopsAdoptionTable', () => {
describe('delete modal integration', () => { describe('delete modal integration', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
wrapper.setData({ selectedSegment: devopsAdoptionSegmentsData.nodes[0] });
}); });
it('re emits trackModalOpenState with the given value', async () => { it('re emits trackModalOpenState with the given value', async () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment