提交 146abb4a 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 a544d1da
<script>
import { omit, throttle } from 'lodash';
import { isEmpty, omit, throttle } from 'lodash';
import { GlLink, GlDeprecatedButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
......@@ -45,6 +45,11 @@ export default {
required: false,
default: () => ({}),
},
timeRange: {
type: Object,
required: false,
default: () => ({}),
},
seriesConfig: {
type: Object,
required: false,
......@@ -174,10 +179,17 @@ export default {
chartOptions() {
const { yAxis, xAxis } = this.option;
const option = omit(this.option, ['series', 'yAxis', 'xAxis']);
const xAxisBounds = isEmpty(this.timeRange)
? {}
: {
min: this.timeRange.start,
max: this.timeRange.end,
};
const timeXAxis = {
...getTimeAxisOptions({ timezone: this.timezone }),
...xAxis,
...xAxisBounds,
};
const dataYAxis = {
......
......@@ -2,6 +2,7 @@
import { mapState } from 'vuex';
import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import {
GlResizeObserverDirective,
......@@ -130,6 +131,15 @@ export default {
return getters[`${this.namespace}/selectedDashboard`];
},
}),
fixedCurrentTimeRange() {
// convertToFixedRange throws an error if the time range
// is not properly set.
try {
return convertToFixedRange(this.timeRange);
} catch {
return {};
}
},
title() {
return this.graphData?.title || '';
},
......@@ -468,6 +478,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
:timezone="dashboardTimezone"
:time-range="fixedCurrentTimeRange"
v-bind="$attrs"
v-on="$listeners"
@datazoom="onDatazoom"
......
<script>
export default {
name: 'FormFieldContainer',
};
</script>
<template>
<div class="row">
<div class="col-md-6 col-lg-5 col-xl-4 gl-display-flex gl-flex-direction-column">
<slot></slot>
</div>
</div>
</template>
......@@ -2,10 +2,11 @@
import { mapState } from 'vuex';
import { uniqueId } from 'lodash';
import { GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
import FormFieldContainer from './form_field_container.vue';
export default {
name: 'TagFieldExisting',
components: { GlFormGroup, GlFormInput, GlSprintf, GlLink },
components: { GlFormGroup, GlFormInput, GlSprintf, GlLink, FormFieldContainer },
computed: {
...mapState('detail', ['release', 'updateReleaseApiDocsPath']),
inputId() {
......@@ -19,18 +20,16 @@ export default {
</script>
<template>
<gl-form-group :label="__('Tag name')" :label-for="inputId">
<div class="row">
<div class="col-md-6 col-lg-5 col-xl-4">
<gl-form-input
:id="inputId"
:value="release.tagName"
type="text"
class="form-control"
:aria-describedby="helpId"
disabled
/>
</div>
</div>
<form-field-container>
<gl-form-input
:id="inputId"
:value="release.tagName"
type="text"
class="form-control"
:aria-describedby="helpId"
disabled
/>
</form-field-container>
<template #description>
<div :id="helpId" data-testid="tag-name-help">
<gl-sprintf
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import FormFieldContainer from './form_field_container.vue';
export default {
name: 'TagFieldNew',
components: { GlFormGroup, GlFormInput, RefSelector, FormFieldContainer },
computed: {
...mapState('detail', ['projectId', 'release', 'createFrom']),
tagName: {
get() {
return this.release.tagName;
},
set(tagName) {
this.updateReleaseTagName(tagName);
},
},
createFromModel: {
get() {
return this.createFrom;
},
set(createFrom) {
this.updateCreateFrom(createFrom);
},
},
tagNameInputId() {
return uniqueId('tag-name-input-');
},
createFromSelectorId() {
return uniqueId('create-from-selector-');
},
},
methods: {
...mapActions('detail', ['updateReleaseTagName', 'updateCreateFrom']),
},
translations: {
noRefSelected: __('No source selected'),
searchPlaceholder: __('Search branches, tags, and commits'),
dropdownHeader: __('Select source'),
},
};
</script>
<template>
<div></div>
<div>
<gl-form-group :label="__('Tag name')" :label-for="tagNameInputId" data-testid="tag-name-field">
<form-field-container>
<gl-form-input :id="tagNameInputId" v-model="tagName" type="text" class="form-control" />
</form-field-container>
</gl-form-group>
<gl-form-group
:label="__('Create from')"
:label-for="createFromSelectorId"
data-testid="create-from-field"
>
<form-field-container>
<ref-selector
:id="createFromSelectorId"
v-model="createFromModel"
:project-id="projectId"
:translations="$options.translations"
/>
</form-field-container>
<template #description>
{{ __('Existing branch name, tag, or commit SHA') }}
</template>
</gl-form-group>
</div>
</template>
......@@ -34,6 +34,10 @@ export const fetchRelease = ({ dispatch, state }) => {
});
};
export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom);
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const updateReleaseMilestones = ({ commit }, milestones) =>
......
......@@ -2,6 +2,8 @@ export const REQUEST_RELEASE = 'REQUEST_RELEASE';
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TAG_NAME = 'UPDATE_RELEASE_TAG_NAME';
export const UPDATE_CREATE_FROM = 'UPDATE_CREATE_FROM';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
......
......@@ -22,6 +22,12 @@ export default {
state.release = undefined;
},
[types.UPDATE_RELEASE_TAG_NAME](state, tagName) {
state.release.tagName = tagName;
},
[types.UPDATE_CREATE_FROM](state, createFrom) {
state.createFrom = createFrom;
},
[types.UPDATE_RELEASE_TITLE](state, title) {
state.release.name = title;
},
......
......@@ -27,6 +27,7 @@ export default ({
releasesPagePath,
defaultBranch,
createFrom: defaultBranch,
/** The Release object */
release: null,
......
---
title: Show full time range in metrics dashboard charts
merge_request: 37243
author:
type: added
......@@ -7024,6 +7024,9 @@ msgstr ""
msgid "Create file"
msgstr ""
msgid "Create from"
msgstr ""
msgid "Create group"
msgstr ""
......@@ -9884,6 +9887,9 @@ msgstr ""
msgid "Excluding merge commits. Limited to 6,000 commits."
msgstr ""
msgid "Existing branch name, tag, or commit SHA"
msgstr ""
msgid "Existing members and groups"
msgstr ""
......@@ -16325,6 +16331,9 @@ msgstr ""
msgid "No schedules"
msgstr ""
msgid "No source selected"
msgstr ""
msgid "No stack trace for this error"
msgstr ""
......@@ -21100,6 +21109,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
msgid "Search branches, tags, and commits"
msgstr ""
msgid "Search by Git revision"
msgstr ""
......@@ -21729,6 +21741,9 @@ msgstr ""
msgid "Select shards to replicate"
msgstr ""
msgid "Select source"
msgstr ""
msgid "Select source branch"
msgstr ""
......
......@@ -12,7 +12,12 @@ import {
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
import {
deploymentData,
mockProjectDir,
annotationsData,
mockFixedTimeRange,
} from '../../mock_data';
import { timeSeriesGraphData } from '../../graph_data';
......@@ -42,6 +47,7 @@ describe('Time series component', () => {
deploymentData,
annotations: annotationsData,
projectPath: `${TEST_HOST}${mockProjectDir}`,
timeRange: mockFixedTimeRange,
...props,
},
stubs: {
......@@ -382,6 +388,25 @@ describe('Time series component', () => {
});
describe('chartOptions', () => {
describe('x-Axis bounds', () => {
it('is set to the time range bounds', () => {
expect(getChartOptions().xAxis).toMatchObject({
min: mockFixedTimeRange.start,
max: mockFixedTimeRange.end,
});
});
it('is not set if time range is not set or incorrectly set', () => {
wrapper.setProps({
timeRange: {},
});
return wrapper.vm.$nextTick(() => {
expect(getChartOptions().xAxis).not.toHaveProperty('min');
expect(getChartOptions().xAxis).not.toHaveProperty('max');
});
});
});
describe('dataZoom', () => {
it('renders with scroll handle icons', () => {
expect(getChartOptions().dataZoom).toHaveLength(1);
......
......@@ -254,6 +254,35 @@ describe('Dashboard Panel', () => {
});
});
});
describe('computed', () => {
describe('fixedCurrentTimeRange', () => {
it('returns fixed time for valid time range', () => {
state.timeRange = mockTimeRange;
return wrapper.vm.$nextTick(() => {
expect(findTimeChart().props('timeRange')).toEqual(
expect.objectContaining({
start: expect.any(String),
end: expect.any(String),
}),
);
});
});
it.each`
input | output
${''} | ${{}}
${undefined} | ${{}}
${null} | ${{}}
${'2020-12-03'} | ${{}}
`('returns $output for invalid input like $input', ({ input, output }) => {
state.timeRange = input;
return wrapper.vm.$nextTick(() => {
expect(findTimeChart().props('timeRange')).toEqual(output);
});
});
});
});
});
describe('Edit custom metric dropdown item', () => {
......
......@@ -343,6 +343,11 @@ export const mockNamespaces = [`${baseNamespace}/1`, `${baseNamespace}/2`];
export const mockTimeRange = { duration: { seconds: 120 } };
export const mockFixedTimeRange = {
start: '2020-06-17T19:59:08.659Z',
end: '2020-07-17T19:59:08.659Z',
};
export const mockNamespacedData = {
mockDeploymentData: ['mockDeploymentData'],
mockProjectPath: '/mockProjectPath',
......
import { shallowMount } from '@vue/test-utils';
import { GlFormInput } from '@gitlab/ui';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createDetailModule from '~/releases/stores/modules/detail';
import RefSelector from '~/ref/components/ref_selector.vue';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from';
describe('releases/components/tag_field_new', () => {
let store;
......@@ -16,9 +22,17 @@ describe('releases/components/tag_field_new', () => {
beforeEach(() => {
store = createStore({
modules: {
detail: createDetailModule({}),
detail: createDetailModule({
projectId: TEST_PROJECT_ID,
}),
},
});
store.state.detail.createFrom = TEST_CREATE_FROM;
store.state.detail.release = {
tagName: TEST_TAG_NAME,
};
});
afterEach(() => {
......@@ -26,9 +40,47 @@ describe('releases/components/tag_field_new', () => {
wrapper = null;
});
it('renders a placeholder component', () => {
createComponent();
const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
const findTagNameGlInput = () => findTagNameFormGroup().find(GlFormInput);
const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]');
const findCreateFromDropdown = () => findCreateFromFormGroup().find(RefSelector);
describe('"Tag name" field', () => {
beforeEach(createComponent);
it('renders a label', () => {
expect(findTagNameFormGroup().attributes().label).toBe('Tag name');
});
describe('when the user updates the field', () => {
it("updates the store's release.tagName property", () => {
const updatedTagName = 'updated-tag-name';
findTagNameGlInput().vm.$emit('input', updatedTagName);
return wrapper.vm.$nextTick().then(() => {
expect(store.state.detail.release.tagName).toBe(updatedTagName);
});
});
});
});
describe('"Create from" field', () => {
beforeEach(createComponent);
expect(wrapper.exists()).toBe(true);
it('renders a label', () => {
expect(findCreateFromFormGroup().attributes().label).toBe('Create from');
});
describe('when the user selects a git ref', () => {
it("updates the store's createFrom property", () => {
const updatedCreateFrom = 'update-create-from';
findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
return wrapper.vm.$nextTick().then(() => {
expect(store.state.detail.createFrom).toBe(updatedCreateFrom);
});
});
});
});
});
......@@ -113,6 +113,24 @@ describe('Release detail actions', () => {
});
});
describe('updateReleaseTagName', () => {
it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
const newTag = 'updated-tag-name';
return testAction(actions.updateReleaseTagName, newTag, state, [
{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag },
]);
});
});
describe('updateCreateFrom', () => {
it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => {
const newRef = 'my-feature-branch';
return testAction(actions.updateCreateFrom, newRef, state, [
{ type: types.UPDATE_CREATE_FROM, payload: newRef },
]);
});
});
describe('updateReleaseTitle', () => {
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
const newTitle = 'The new release title';
......
......@@ -56,6 +56,26 @@ describe('Release detail mutations', () => {
});
});
describe(`${types.UPDATE_RELEASE_TAG_NAME}`, () => {
it("updates the release's tag name", () => {
state.release = release;
const newTag = 'updated-tag-name';
mutations[types.UPDATE_RELEASE_TAG_NAME](state, newTag);
expect(state.release.tagName).toBe(newTag);
});
});
describe(`${types.UPDATE_CREATE_FROM}`, () => {
it('updates the ref that the ref will be created from', () => {
state.createFrom = 'main';
const newRef = 'my-feature-branch';
mutations[types.UPDATE_CREATE_FROM](state, newRef);
expect(state.createFrom).toBe(newRef);
});
});
describe(`${types.UPDATE_RELEASE_TITLE}`, () => {
it("updates the release's title", () => {
state.release = release;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册