Commit 7da363cc authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '34021-add-searchable-dropdown-for-monitor-environments' into 'master'

Environments dropdown only shows 20 environments

See merge request gitlab-org/gitlab!23017
parents ac23953c c6a7a8d7
......@@ -8,6 +8,7 @@ import {
GlDropdownItem,
GlFormGroup,
GlModal,
GlSearchBoxByType,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
......@@ -15,6 +16,7 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
......@@ -38,6 +40,7 @@ export default {
GlButton,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlFormGroup,
GlModal,
......@@ -52,6 +55,7 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
externalDashboardUrl: {
type: String,
......@@ -198,13 +202,12 @@ export default {
'dashboard',
'emptyState',
'showEmptyState',
'environments',
'deploymentData',
'useDashboardEndpoint',
'allDashboards',
'additionalPanelTypesEnabled',
]),
...mapGetters('monitoringDashboard', ['getMetricStates']),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
......@@ -227,6 +230,9 @@ export default {
this.externalDashboardUrl.length
);
},
shouldRenderSearchableEnvironmentsDropdown() {
return this.glFeatures.searchableEnvironmentsDropdown;
},
},
created() {
this.setEndpoints({
......@@ -255,6 +261,7 @@ export default {
'setGettingStartedEmptyState',
'setEndpoints',
'setPanelGroupMetrics',
'setEnvironmentsSearchTerm',
]),
updatePanels(key, panels) {
this.setPanelGroupMetrics({
......@@ -296,6 +303,9 @@ export default {
setFormValidity(isValid) {
this.formIsValid = isValid;
},
debouncedEnvironmentsSearch: _.debounce(function environmentsSearchOnInput(searchTerm) {
this.setEnvironmentsSearchTerm(searchTerm);
}, 500),
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
......@@ -374,17 +384,36 @@ export default {
data-qa-selector="environments_dropdown"
class="mb-0 d-flex"
toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu"
:text="currentEnvironmentName"
:disabled="environments.length === 0"
:disabled="filteredEnvironments.length === 0"
>
<div class="d-flex flex-column overflow-hidden">
<gl-search-box-by-type
v-if="shouldRenderSearchableEnvironmentsDropdown"
ref="monitorEnvironmentsDropdownSearch"
class="m-2"
@input="debouncedEnvironmentsSearch"
/>
<div class="flex-fill overflow-auto">
<gl-dropdown-item
v-for="environment in environments"
v-for="environment in filteredEnvironments"
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
:href="environment.metrics_path"
>{{ environment.name }}</gl-dropdown-item
>
</div>
<div
v-if="shouldRenderSearchableEnvironmentsDropdown"
v-show="filteredEnvironments.length === 0"
ref="monitorEnvironmentsDropdownMsg"
class="text-secondary no-matches-message"
>
{{ s__('No matching results') }}
</div>
</div>
</gl-dropdown>
</gl-form-group>
......@@ -415,18 +444,16 @@ export default {
variant="default"
class="mr-2 mt-1 js-rearrange-button"
@click="toggleRearrangingPanels"
>{{ __('Arrange charts') }}</gl-button
>
{{ __('Arrange charts') }}
</gl-button>
<gl-button
v-if="addingMetricsAvailable"
ref="addMetricBtn"
v-gl-modal="$options.addMetric.modalId"
variant="outline-success"
class="mr-2 mt-1"
>{{ $options.addMetric.title }}</gl-button
>
{{ $options.addMetric.title }}
</gl-button>
<gl-modal
v-if="addingMetricsAvailable"
ref="addMetricModal"
......@@ -448,9 +475,8 @@ export default {
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
>{{ __('Save changes') }}</gl-button
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-modal>
......@@ -458,9 +484,8 @@ export default {
v-if="selectedDashboard.can_edit"
class="mt-1 js-edit-link"
:href="selectedDashboard.project_blob_path"
>{{ __('Edit dashboard') }}</gl-button
>
{{ __('Edit dashboard') }}
</gl-button>
<gl-button
v-if="externalDashboardUrl.length"
......@@ -506,9 +531,9 @@ export default {
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
@click="removePanel(groupData.key, groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
/></a>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
<icon name="close" />
</a>
</div>
<panel-type
......
......@@ -30,6 +30,10 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
export const setEnvironmentsSearchTerm = ({ commit }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_SEARCH_TERM, searchTerm);
};
export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
......
......@@ -58,5 +58,20 @@ export const metricsWithData = state => groupKey => {
return res;
};
/**
* Filter environments by names.
*
* This is used in the environments dropdown with searchable input.
* Also, this searchable dropdown is behind `searchable_environments_dropdown`
* feature flag
*
* @param {Object} state
* @returns {Array} List of environments
*/
export const filteredEnvironments = state =>
state.environments.filter(env =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -21,3 +21,5 @@ export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
export const SET_ENVIRONMENTS_SEARCH_TERM = 'SET_ENVIRONMENTS_SEARCH_TERM';
......@@ -196,4 +196,7 @@ export default {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
panelGroup.panels = payload.panels;
},
[types.SET_ENVIRONMENTS_SEARCH_TERM](state, searchTerm) {
state.environmentsSearchTerm = searchTerm;
},
};
......@@ -15,6 +15,7 @@ export default () => ({
deploymentData: [],
environments: [],
environmentsSearchTerm: '',
allDashboards: [],
currentDashboard: null,
projectPath: null,
......
......@@ -58,6 +58,18 @@
.custom-time-range-form-group > label {
padding-bottom: $gl-padding;
}
.monitor-environment-dropdown-menu {
&.show {
display: flex;
flex-direction: column;
overflow: hidden;
}
.no-matches-message {
padding: $gl-padding-8 $gl-padding-12;
}
}
}
.prometheus-panel {
......
......@@ -14,6 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:searchable_environments_dropdown)
end
before_action do
push_frontend_feature_flag(:auto_stop_environments)
......
......@@ -28,6 +28,15 @@ describe('Dashboard', () => {
let wrapper;
let mock;
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
const setSearchTerm = searchTerm => {
wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ENVIRONMENTS_SEARCH_TERM}`,
searchTerm,
);
};
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
localVue,
......@@ -52,9 +61,6 @@ describe('Dashboard', () => {
});
};
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
......@@ -155,12 +161,9 @@ describe('Dashboard', () => {
wrapper.vm
.$nextTick()
.then(() => {
const environmentDropdownItems = findAllEnvironmentsDropdownItems();
expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
expect(wrapper.vm.environments.length).toEqual(environmentData.length);
expect(environmentDropdownItems.length).toEqual(wrapper.vm.environments.length);
environmentDropdownItems.wrappers.forEach((itemWrapper, index) => {
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
const anchorEl = itemWrapper.find('a');
if (anchorEl.exists() && environmentData[index].metrics_path) {
const href = anchorEl.attributes('href');
......@@ -248,6 +251,70 @@ describe('Dashboard', () => {
});
});
describe('searchable environments dropdown', () => {
beforeEach(() => {
createMountedWrapper(
{ hasMetrics: true },
{
attachToDocument: true,
stubs: ['graph-group', 'panel-type'],
provide: {
glFeatures: { searchableEnvironmentsDropdown: true },
},
},
);
setupComponentStore(wrapper);
return wrapper.vm.$nextTick();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a search input', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownSearch' }).exists()).toBe(true);
});
it('renders dropdown items', () => {
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
const anchorEl = itemWrapper.find('a');
if (anchorEl.exists()) {
expect(anchorEl.text()).toBe(environmentData[index].name);
}
});
});
it('filters rendered dropdown items', () => {
const searchTerm = 'production';
const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length);
});
});
it('does not filter dropdown items if search term is empty string', () => {
const searchTerm = '';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
});
});
it("shows error message if search term doesn't match", () => {
const searchTerm = 'does-not-exist';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }).isVisible()).toBe(true);
});
});
});
describe('drag and drop function', () => {
const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
......
......@@ -331,6 +331,14 @@ export const mockedQueryResultPayloadCoresTotal = {
],
};
const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({
id: 136 + idx,
name: `no-deployment/noop-branch-${idx}`,
state: 'available',
created_at: '2018-07-04T18:39:41.702Z',
updated_at: '2018-07-04T18:44:54.010Z',
}));
export const environmentData = [
{
id: 34,
......@@ -368,14 +376,7 @@ export const environmentData = [
id: 128,
},
},
{
id: 36,
name: 'no-deployment/noop-branch',
state: 'available',
created_at: '2018-07-04T18:39:41.702Z',
updated_at: '2018-07-04T18:44:54.010Z',
},
];
].concat(extraEnvironmentData);
export const metricsDashboardResponse = {
dashboard: {
......
......@@ -3,6 +3,7 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import {
environmentData,
metricsDashboardPayload,
mockedEmptyResult,
mockedQueryResultPayload,
......@@ -214,4 +215,58 @@ describe('Monitoring store Getters', () => {
});
});
});
describe('filteredEnvironments', () => {
let state;
const setupState = (initState = {}) => {
state = {
...state,
...initState,
};
};
beforeAll(() => {
setupState({
environments: environmentData,
});
});
afterAll(() => {
state = null;
});
[
{
input: '',
output: 17,
},
{
input: ' ',
output: 17,
},
{
input: null,
output: 17,
},
{
input: 'does-not-exist',
output: 0,
},
{
input: 'noop-branch-',
output: 15,
},
{
input: 'noop-branch-9',
output: 1,
},
].forEach(({ input, output }) => {
it(`filteredEnvironments returns ${output} items for ${input}`, () => {
setupState({
environmentsSearchTerm: input,
});
expect(getters.filteredEnvironments(state).length).toBe(output);
});
});
});
});
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