Commit b1a936ba authored by Mark Florian's avatar Mark Florian

Merge branch '299126-reusable-form-components' into 'master'

Reusable security form components

See merge request gitlab-org/gitlab!52587
parents 273cf362 f2c7a315
<script>
import { GlFormGroup, GlDropdown, GlDropdownItem, GlFormText, GlLink, GlSprintf } from '@gitlab/ui';
import { CUSTOM_VALUE_MESSAGE } from './constants';
export default {
components: {
GlFormGroup,
GlDropdown,
GlDropdownItem,
GlFormText,
GlLink,
GlSprintf,
},
// The DynamicFields component v-binds the configuration entity to this
// component. This ensures extraneous keys/values are not added as attributes
// to the underlying GlFormGroup.
inheritAttrs: false,
model: {
prop: 'value',
event: 'input',
},
props: {
value: {
type: String,
required: true,
},
defaultValue: {
type: String,
required: false,
default: null,
},
field: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
defaultText: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
options: {
type: Array,
required: true,
validator: (options) =>
options.every(({ value, text }) => ![value, text].includes(undefined)),
},
},
computed: {
showCustomValueMessage() {
return this.defaultValue !== null && !this.disabled && this.value !== this.defaultValue;
},
text() {
return this.options.find((option) => option.value === this.value)?.text || this.defaultText;
},
},
methods: {
resetToDefaultValue() {
this.$emit('input', this.defaultValue);
},
handleInput(option) {
this.$emit('input', option.value);
},
},
i18n: {
CUSTOM_VALUE_MESSAGE,
},
};
</script>
<template>
<gl-form-group :label-for="field">
<template #label>
{{ label }}
<gl-form-text class="gl-mt-3">{{ description }}</gl-form-text>
</template>
<gl-dropdown :id="field" :text="text" :disabled="disabled">
<gl-dropdown-item v-for="option in options" :key="option.value" @click="handleInput(option)">
{{ option.text }}
</gl-dropdown-item>
</gl-dropdown>
<template v-if="showCustomValueMessage" #description>
<gl-sprintf :message="$options.i18n.CUSTOM_VALUE_MESSAGE">
<template #anchor="{ content }">
<gl-link @click="resetToDefaultValue" v-text="content" />
</template>
</gl-sprintf>
</template>
</gl-form-group>
</template>
<script>
import { GlFormGroup } from '@gitlab/ui';
import FormInput from './form_input.vue';
import DropdownInput from './dropdown_input.vue';
import { isValidConfigurationEntity } from './utils';
export default {
......@@ -49,6 +50,7 @@ export default {
// before the frontend adds support for them.
entityTypeToComponent: {
string: FormInput,
select: DropdownInput,
},
};
</script>
......
......@@ -25,7 +25,8 @@ export default {
},
defaultValue: {
type: String,
required: true,
required: false,
default: null,
},
field: {
type: String,
......@@ -53,7 +54,7 @@ export default {
},
computed: {
showCustomValueMessage() {
return !this.disabled && this.value !== this.defaultValue;
return this.defaultValue !== null && !this.disabled && this.value !== this.defaultValue;
},
inputSize() {
return SCHEMA_TO_PROP_SIZE_MAP[this.size];
......
const isString = (value) => typeof value === 'string';
const isBoolean = (value) => typeof value === 'boolean';
export const isValidConfigurationEntity = (object) => {
if (object == null) {
return false;
}
const { field, type, description, label, value } = object;
return (
isString(field) &&
isString(type) &&
isString(description) &&
isString(label) &&
value !== undefined
);
};
export const isValidAnalyzerEntity = (object) => {
if (object == null) {
return false;
}
const { name, label, enabled } = object;
return isString(name) && isString(label) && isBoolean(enabled);
};
<script>
import { GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DynamicFields from './dynamic_fields.vue';
import { isValidAnalyzerEntity } from './utils';
import DynamicFields from '../../components/dynamic_fields.vue';
import { isValidAnalyzerEntity } from '../../components/utils';
export default {
components: {
......
......@@ -6,8 +6,8 @@ import { __, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
import AnalyzerConfiguration from './analyzer_configuration.vue';
import DynamicFields from './dynamic_fields.vue';
import ExpandableSection from './expandable_section.vue';
import DynamicFields from '../../components/dynamic_fields.vue';
import ExpandableSection from '../../components/expandable_section.vue';
import {
toSastCiConfigurationEntityInput,
toSastCiConfigurationAnalyzerEntityInput,
......
const isString = (value) => typeof value === 'string';
const isBoolean = (value) => typeof value === 'boolean';
export const isValidConfigurationEntity = (object) => {
if (object == null) {
return false;
}
const { field, type, description, label, defaultValue, value } = object;
return (
isString(field) &&
isString(type) &&
isString(description) &&
isString(label) &&
defaultValue !== undefined &&
value !== undefined
);
};
export const isValidAnalyzerEntity = (object) => {
if (object == null) {
return false;
}
const { name, label, enabled } = object;
return isString(name) && isString(label) && isBoolean(enabled);
};
/**
* Given a SastCiConfigurationEntity, returns a SastCiConfigurationEntityInput
* suitable for use in the configureSast GraphQL mutation.
......
import { GlDropdown, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import GlDropdownInput from 'ee/security_configuration/components/dropdown_input.vue';
describe('DropdownInput component', () => {
let wrapper;
const option1 = {
value: 'option1',
text: 'Option 1',
};
const option2 = {
value: 'option2',
text: 'Option 2',
};
const testProps = {
field: 'field',
label: 'label',
description: 'description',
defaultValue: option1.value,
defaultText: 'defaultText',
value: option1.value,
options: [option1, option2],
};
const newValue = 'foo';
const createComponent = ({ props = {} } = {}) => {
wrapper = mount(GlDropdownInput, {
propsData: {
...props,
},
});
};
const findToggle = () => wrapper.find('button');
const findLabel = () => wrapper.find('label');
const findInputComponent = () => wrapper.find(GlDropdown);
const findRestoreDefaultLink = () => wrapper.find(GlLink);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('label', () => {
beforeEach(() => {
createComponent({
props: testProps,
});
});
it('renders the label', () => {
expect(findLabel().text()).toContain(testProps.label);
});
it('renders the description', () => {
expect(findLabel().text()).toContain(testProps.description);
});
});
describe('input', () => {
beforeEach(() => {
createComponent({
props: testProps,
});
});
it('sets the input to the value', () => {
expect(findToggle().text()).toBe(option1.text);
});
it('is connected to the label', () => {
expect(findInputComponent().attributes('id')).toBe(testProps.field);
expect(findLabel().attributes('for')).toBe(testProps.field);
});
describe('when the user changes the value', () => {
beforeEach(() => {
wrapper.findAll('li').at(1).find('button').trigger('click');
});
it('emits an input event with the new value', () => {
expect(wrapper.emitted('input')).toEqual([[option2.value]]);
});
});
});
describe('custom value message', () => {
describe('given the value equals the default value', () => {
beforeEach(() => {
createComponent({
props: testProps,
});
});
it('does not display the custom value message', () => {
expect(findRestoreDefaultLink().exists()).toBe(false);
});
});
describe('given the value differs from the default value', () => {
beforeEach(() => {
createComponent({
props: {
...testProps,
value: newValue,
},
});
});
it('displays the custom value message', () => {
expect(findRestoreDefaultLink().exists()).toBe(true);
});
describe('clicking on the restore default link', () => {
beforeEach(() => {
findRestoreDefaultLink().trigger('click');
});
it('emits an input event with the default value', () => {
expect(wrapper.emitted('input')).toEqual([[testProps.defaultValue]]);
});
});
describe('disabling the input', () => {
beforeEach(async () => {
await wrapper.setProps({ disabled: true });
});
it('does not display the custom value message', () => {
expect(findRestoreDefaultLink().exists()).toBe(false);
});
});
});
});
});
import { shallowMount, mount } from '@vue/test-utils';
import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import { makeEntities } from '../helpers';
describe('DynamicFields component', () => {
......
import { mount, shallowMount } from '@vue/test-utils';
import { stubTransition } from 'helpers/stub_transition';
import ExpandableSection from 'ee/security_configuration/sast/components/expandable_section.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue';
describe('ExpandableSection component', () => {
let wrapper;
......
import { GlFormInput, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { SCHEMA_TO_PROP_SIZE_MAP } from 'ee/security_configuration/sast/components/constants';
import FormInput from 'ee/security_configuration/sast/components/form_input.vue';
import { SCHEMA_TO_PROP_SIZE_MAP } from 'ee/security_configuration/components/constants';
import FormInput from 'ee/security_configuration/components/form_input.vue';
describe('FormInput component', () => {
let wrapper;
......
import {
isValidConfigurationEntity,
isValidAnalyzerEntity,
} from 'ee/security_configuration/components/utils';
import { makeEntities, makeAnalyzerEntities } from '../helpers';
describe('isValidConfigurationEntity', () => {
const validEntities = makeEntities(3);
const invalidEntities = [
null,
undefined,
[],
{},
...makeEntities(1, { field: undefined }),
...makeEntities(1, { type: undefined }),
...makeEntities(1, { description: undefined }),
...makeEntities(1, { label: undefined }),
...makeEntities(1, { value: undefined }),
];
it.each(validEntities)('returns true for a valid entity', (entity) => {
expect(isValidConfigurationEntity(entity)).toBe(true);
});
it.each(invalidEntities)('returns false for an invalid entity', (invalidEntity) => {
expect(isValidConfigurationEntity(invalidEntity)).toBe(false);
});
});
describe('isValidAnalyzerEntity', () => {
const validEntities = makeAnalyzerEntities(3);
const invalidEntities = [
null,
undefined,
[],
{},
...makeAnalyzerEntities(1, { name: undefined }),
...makeAnalyzerEntities(1, { label: undefined }),
...makeAnalyzerEntities(1, { enabled: undefined }),
...makeAnalyzerEntities(1, { enabled: '' }),
];
it.each(validEntities)('returns true for a valid entity', (entity) => {
expect(isValidAnalyzerEntity(entity)).toBe(true);
});
it.each(invalidEntities)('returns false for an invalid entity', (invalidEntity) => {
expect(isValidAnalyzerEntity(invalidEntity)).toBe(false);
});
});
import { mount } from '@vue/test-utils';
import AnalyzerConfiguration from 'ee/security_configuration/sast/components/analyzer_configuration.vue';
import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue';
import { makeAnalyzerEntities, makeEntities, makeSastCiConfiguration } from '../helpers';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import { makeAnalyzerEntities, makeEntities, makeSastCiConfiguration } from '../../helpers';
describe('AnalyzerConfiguration component', () => {
let wrapper;
......
......@@ -3,12 +3,12 @@ import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import AnalyzerConfiguration from 'ee/security_configuration/sast/components/analyzer_configuration.vue';
import ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue';
import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/sast/components/expandable_section.vue';
import DynamicFields from 'ee/security_configuration/components/dynamic_fields.vue';
import ExpandableSection from 'ee/security_configuration/components/expandable_section.vue';
import configureSastMutation from 'ee/security_configuration/sast/graphql/configure_sast.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper';
import { makeEntities, makeSastCiConfiguration } from '../helpers';
import { makeEntities, makeSastCiConfiguration } from '../../helpers';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
......
import {
isValidConfigurationEntity,
isValidAnalyzerEntity,
toSastCiConfigurationEntityInput,
toSastCiConfigurationAnalyzerEntityInput,
} from 'ee/security_configuration/sast/components/utils';
import { makeEntities, makeAnalyzerEntities } from '../helpers';
describe('isValidConfigurationEntity', () => {
const validEntities = makeEntities(3);
const invalidEntities = [
null,
undefined,
[],
{},
...makeEntities(1, { field: undefined }),
...makeEntities(1, { type: undefined }),
...makeEntities(1, { description: undefined }),
...makeEntities(1, { label: undefined }),
...makeEntities(1, { value: undefined }),
...makeEntities(1, { defaultValue: undefined }),
];
it.each(validEntities)('returns true for a valid entity', (entity) => {
expect(isValidConfigurationEntity(entity)).toBe(true);
});
it.each(invalidEntities)('returns false for an invalid entity', (invalidEntity) => {
expect(isValidConfigurationEntity(invalidEntity)).toBe(false);
});
});
describe('isValidAnalyzerEntity', () => {
const validEntities = makeAnalyzerEntities(3);
const invalidEntities = [
null,
undefined,
[],
{},
...makeAnalyzerEntities(1, { name: undefined }),
...makeAnalyzerEntities(1, { label: undefined }),
...makeAnalyzerEntities(1, { enabled: undefined }),
...makeAnalyzerEntities(1, { enabled: '' }),
];
it.each(validEntities)('returns true for a valid entity', (entity) => {
expect(isValidAnalyzerEntity(entity)).toBe(true);
});
it.each(invalidEntities)('returns false for an invalid entity', (invalidEntity) => {
expect(isValidAnalyzerEntity(invalidEntity)).toBe(false);
});
});
import { makeEntities, makeAnalyzerEntities } from '../../helpers';
describe('toSastCiConfigurationEntityInput', () => {
let entity;
......
import { makeEntities } from './helpers';
import { makeEntities } from '../helpers';
export const sastCiConfigurationQueryResponse = {
data: {
......
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