From 652d8b33ee36e9977c03d1ed7da12ddc03292cf4 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 19 Nov 2021 21:12:59 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/issue_templates/Experiment Rollout.md | 2 +- .../components/chronic_duration_input.vue | 133 ++++++ doc/user/group/devops_adoption/index.md | 123 +++--- locale/gitlab.pot | 6 + package.json | 2 +- .../3_create/wiki/content_editor_spec.rb | 2 +- .../components/chronic_duration_input_spec.js | 390 ++++++++++++++++++ yarn.lock | 8 +- 8 files changed, 591 insertions(+), 75 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/chronic_duration_input.vue create mode 100644 spec/frontend/vue_shared/components/chronic_duration_input_spec.js diff --git a/.gitlab/issue_templates/Experiment Rollout.md b/.gitlab/issue_templates/Experiment Rollout.md index 9209423ba33..a7d6b46220e 100644 --- a/.gitlab/issue_templates/Experiment Rollout.md +++ b/.gitlab/issue_templates/Experiment Rollout.md @@ -82,7 +82,7 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate. ## Roll Out Steps -- [ ] Confirm that QA tests pass with the feature flag enabled (if you're unsure how, contact the relevant [stable counterpart in the Quality department](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors)) +- [ ] [Confirm that end-to-end tests pass with the feature flag enabled](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/feature_flags.html#confirming-that-end-to-end-tests-pass-with-a-feature-flag-enabled). If there are failing tests, contact the relevant [stable counterpart in the Quality department](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors) to collaborate in updating the tests or confirming that the failing tests are not caused by the changes behind the enabled feature flag. - [ ] Enable on staging (`/chatops run feature set true --staging`) - [ ] Test on staging - [ ] Ensure that documentation has been updated diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue new file mode 100644 index 00000000000..ffbcdefc924 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue @@ -0,0 +1,133 @@ + + diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md index 36ccfc1031f..c67fa4abfc5 100644 --- a/doc/user/group/devops_adoption/index.md +++ b/doc/user/group/devops_adoption/index.md @@ -16,94 +16,81 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - Overview table [added](https://gitlab.com/gitlab-org/gitlab/-/issues/335638) in GitLab 14.3. > - Adoption over time chart [added](https://gitlab.com/gitlab-org/gitlab/-/issues/337561) in GitLab 14.4. -Prerequisites: +DevOps Adoption shows you how groups in your organization adopt and use the most essential features of GitLab. -- You must have at least the [Reporter role](../../permissions.md) for the group. +You can use Group DevOps Adoption to: -To access Group DevOps Adoption, go to your group and select **Analytics > DevOps Adoption**. - -Group DevOps Adoption shows you how individual groups and subgroups within your organization use the following features: - -- Dev - - Approvals - - Code owners - - Issues - - Merge requests -- Sec - - DAST - - Dependency Scanning - - Fuzz Testing - - SAST -- Ops - - Deployments - - Pipelines - - Runners - -When managing groups in the UI, you can add or remove your subgroups with the **Add or remove subgroups** -button, in the top right hand section of your Groups pages. - -With DevOps Adoption you can: - -- Verify whether you are getting the return on investment that you expected from GitLab. -- Identify specific subgroups that are lagging in their adoption of GitLab, so you can help them along in their DevOps journey. -- Find the subgroups that have adopted certain features, and can provide guidance to other subgroups on how to use those features. +- Identify specific subgroups that are lagging in their adoption of GitLab features, so you can guide them on +their DevOps journey. +- Find subgroups that have adopted certain features, and provide guidance to other subgroups on +how to use those features. +- Verify if you are getting the return on investment that you expected from GitLab. ![DevOps Adoption](img/group_devops_adoption_v14_2.png) -Feature adoption is based on usage in the previous calendar month. Data is updated on the first day -of each month. If the monthly update fails, it tries again daily until successful. +## View DevOps Adoption -## Enable data processing +Prerequisite: -Group DevOps Adoption relies on data that has been gathered by a weekly data processing task. -This task is disabled by default. +- You must have at least the [Reporter role](../../permissions.md) for the group. -To begin using Group DevOps Adoption, access the feature for the first time. GitLab automatically -enables the data processing for that group. The group data doesn't appear immediately, because -GitLab requires around a minute to process it. +To view DevOps Adoption: -## What is displayed +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Analytics > DevOps adoption** -DevOps Adoption displays feature adoption data for the given group -and any added subgroups for the current calendar month. -Each group appears as a separate row in the table. -For each row, a feature is considered "adopted" if it has been used in a project in the given group -during the time period (including projects in any subgroups of the given group). +## DevOps Adoption categories -## Adoption over time +DevOps Adoption shows feature adoption for development, security, and operations. -The **Adoption over time** chart in the **Overview** tab displays DevOps Adoption over time. The chart displays the total number of adopted features from the previous twelve months, -from when you enabled DevOps Adoption for the group. +| Category | Feature | +| --- | --- | +| Development | Approvals
Code owners
Issues
Merge requests | +| Security | DAST
Dependency Scanning
Fuzz Testing
SAST | +| Operations | Deployments
Pipelines
Runners | -The tooltip displays information about the features tracked for individual months. +## Feature adoption -## When is a feature considered adopted +DevOps Adoption shows feature adoption data for groups and subgroups for the previous calendar month. -A feature is considered "adopted" if it has been used anywhere in the group in the specified time. -For example, if an issue was created in one project in a group, the group is considered to have -"adopted" issues in that time. +A feature shows as **adopted** when a group has used the feature in a project during the time period. +This includes projects in any subgroups of the group. For example, if an issue was created in a project in a group, the group has adopted issues in that time. -## No penalties for common adoption patterns +### Exceptions to feature adoption data -DevOps Adoption is designed not to penalize for any circumstances or practices that are common in DevOps. -Following this guideline, GitLab doesn't penalize for: +When GitLab measures DevOps Adoption, some common DevOps information is not included: -1. Having dormant projects. It's common for groups to have a mix of active and dormant projects, - so we should not consider adoption to be low if there are relatively many dormant projects. - This means we should not measure adoption by how many projects in the group have used a feature, - only by whether a feature was used anywhere in the group. -1. GitLab adding new features over time. It's common for group feature usage to be consistent - over time, so we should not consider adoption to have decreased if GitLab adds features. - This means we should not measure adoption by percentages, only total counts. +- Dormant projects. It doesn't matter how many projects in the group use a feature. Even if you have many dormant projects, it doesn't lower the adoption. +- New GitLab features. Adoption is the total number of features adopted, not the percent of features. -## Add a subgroup +## When DevOps Adoption data is gathered -DevOps Adoption can also display data for subgroups within the given group, -to show you differences in adoption across the group. -To add subgroups to your Group DevOps Adoption report: +A weekly task processes data for DevOps Adoption. This task is disabled until you access +DevOps Adoption for a group for the first time. + +The data processing task updates the data on the first day of each month. If the monthly update +fails, the task tries daily until it succeeds. + +DevOps Adoption data may take up to a minute to appear while GitLab processes the group's data. + +## View feature adoption over time + +The **Adoption over time** chart shows the total number of adopted features from the previous +twelve months. The chart only shows data from when you enabled DevOps Adoption for the group. + +To view feature adoption over time: + +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Analytics > DevOps adoption**. +1. Select the **Overview** tab. + +Tooltips display information about the features tracked for individual months. + +## Add or remove a subgroup + +To add or remove a subgroup from the DevOps Adoption report: 1. Select **Add or remove subgroups**. -1. Select the subgroups you want to add and select **Save changes**. +1. Select the subgroup you want to add or remove and select **Save changes**. -The subgroup data might not appear immediately, because GitLab requires around a minute to collect -the data. +It may take up to a minute for subgroup data to appear while GitLab collects the data. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2c73e384755..f0e3bcc8cb1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3917,6 +3917,9 @@ msgstr "" msgid "An example showing how to use Jsonnet with GitLab dynamic child pipelines" msgstr "" +msgid "An integer value is required for seconds" +msgstr "" + msgid "An issue already exists" msgstr "" @@ -25974,6 +25977,9 @@ msgstr "" msgid "Please enter a valid number" msgstr "" +msgid "Please enter a valid time interval" +msgstr "" + msgid "Please enter or upload a valid license." msgstr "" diff --git a/package.json b/package.json index c76c46b4470..682b8b37e23 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "1.221.0", "@gitlab/tributejs": "1.0.0", - "@gitlab/ui": "32.38.0", + "@gitlab/ui": "32.39.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "6.1.4-1", "@rails/ujs": "6.1.4-1", diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb index bd4b82d8ea0..295c5cbdec3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346149', type: :stale } do context 'Content Editor' do let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! } let(:page_title) { 'Content Editor Page' } diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js new file mode 100644 index 00000000000..530d01402c6 --- /dev/null +++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js @@ -0,0 +1,390 @@ +import { mount } from '@vue/test-utils'; +import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue'; + +const MOCK_VALUE = 2 * 3600 + 20 * 60; + +describe('vue_shared/components/chronic_duration_input', () => { + let wrapper; + let textElement; + let hiddenElement; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + textElement = null; + hiddenElement = null; + }); + + const findComponents = () => { + textElement = wrapper.find('input[type=text]').element; + hiddenElement = wrapper.find('input[type=hidden]').element; + }; + + const createComponent = (props = {}) => { + if (wrapper) { + throw new Error('There should only be one wrapper created per test'); + } + + wrapper = mount(ChronicDurationInput, { propsData: props }); + findComponents(); + }; + + describe('value', () => { + it('has human-readable output with value', () => { + createComponent({ value: MOCK_VALUE }); + + expect(textElement.value).toBe('2 hrs 20 mins'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + + it('has empty output with no value', () => { + createComponent({ value: null }); + + expect(textElement.value).toBe(''); + expect(hiddenElement.value).toBe(''); + }); + }); + + describe('change', () => { + const createAndDispatch = async (initialValue, humanReadableInput) => { + createComponent({ value: initialValue }); + await wrapper.vm.$nextTick(); + textElement.value = humanReadableInput; + textElement.dispatchEvent(new Event('input')); + }; + + describe('when starting with no value and receiving human-readable input', () => { + beforeEach(() => { + createAndDispatch(null, '2hr20min'); + }); + + it('updates hidden field', () => { + expect(textElement.value).toBe('2hr20min'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + + it('emits change event', () => { + expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); + }); + }); + + describe('when starting with a value and receiving empty input', () => { + beforeEach(() => { + createAndDispatch(MOCK_VALUE, ''); + }); + + it('updates hidden field', () => { + expect(textElement.value).toBe(''); + expect(hiddenElement.value).toBe(''); + }); + + it('emits change event', () => { + expect(wrapper.emitted('change')).toEqual([[null]]); + }); + }); + + describe('when starting with a value and receiving invalid input', () => { + beforeEach(() => { + createAndDispatch(MOCK_VALUE, 'gobbledygook'); + }); + + it('does not update hidden field', () => { + expect(textElement.value).toBe('gobbledygook'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + + it('does not emit change event', () => { + expect(wrapper.emitted('change')).toBeUndefined(); + }); + }); + }); + + describe('valid', () => { + describe('initial value', () => { + beforeEach(() => { + createComponent({ value: MOCK_VALUE }); + }); + + it('emits valid with initial value', () => { + expect(wrapper.emitted('valid')).toEqual([[{ valid: true, feedback: '' }]]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + + it('emits valid with user input', async () => { + textElement.value = '1m10s'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: true, feedback: '' }], + [{ valid: true, feedback: '' }], + ]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + + textElement.value = ''; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: true, feedback: '' }], + [{ valid: true, feedback: '' }], + [{ valid: null, feedback: '' }], + ]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + + it('emits invalid with user input', async () => { + textElement.value = 'gobbledygook'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: true, feedback: '' }], + [{ valid: false, feedback: ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK }], + ]); + expect(textElement.validity.valid).toBe(false); + expect(textElement.validity.customError).toBe(true); + expect(textElement.validationMessage).toBe( + ChronicDurationInput.i18n.INVALID_INPUT_FEEDBACK, + ); + expect(hiddenElement.validity.valid).toBe(false); + expect(hiddenElement.validity.customError).toBe(true); + // Hidden elements do not have validationMessage + expect(hiddenElement.validationMessage).toBe(''); + }); + }); + + describe('no initial value', () => { + beforeEach(() => { + createComponent({ value: null }); + }); + + it('emits valid with no initial value', () => { + expect(wrapper.emitted('valid')).toEqual([[{ valid: null, feedback: '' }]]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + + it('emits valid with updated value', async () => { + wrapper.setProps({ value: MOCK_VALUE }); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: null, feedback: '' }], + [{ valid: true, feedback: '' }], + ]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + }); + + describe('decimal input', () => { + describe('when integerRequired is false', () => { + beforeEach(() => { + createComponent({ value: null, integerRequired: false }); + }); + + it('emits valid when input is integer', async () => { + textElement.value = '2hr20min'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: null, feedback: '' }], + [{ valid: true, feedback: '' }], + ]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + + it('emits valid when input is decimal', async () => { + textElement.value = '1.5s'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('change')).toEqual([[1.5]]); + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: null, feedback: '' }], + [{ valid: true, feedback: '' }], + ]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + }); + + describe('when integerRequired is unspecified', () => { + beforeEach(() => { + createComponent({ value: null }); + }); + + it('emits valid when input is integer', async () => { + textElement.value = '2hr20min'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]); + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: null, feedback: '' }], + [{ valid: true, feedback: '' }], + ]); + expect(textElement.validity.valid).toBe(true); + expect(textElement.validity.customError).toBe(false); + expect(textElement.validationMessage).toBe(''); + expect(hiddenElement.validity.valid).toBe(true); + expect(hiddenElement.validity.customError).toBe(false); + expect(hiddenElement.validationMessage).toBe(''); + }); + + it('emits invalid when input is decimal', async () => { + textElement.value = '1.5s'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('change')).toBeUndefined(); + expect(wrapper.emitted('valid')).toEqual([ + [{ valid: null, feedback: '' }], + [ + { + valid: false, + feedback: ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK, + }, + ], + ]); + expect(textElement.validity.valid).toBe(false); + expect(textElement.validity.customError).toBe(true); + expect(textElement.validationMessage).toBe( + ChronicDurationInput.i18n.INVALID_DECIMAL_FEEDBACK, + ); + expect(hiddenElement.validity.valid).toBe(false); + expect(hiddenElement.validity.customError).toBe(true); + // Hidden elements do not have validationMessage + expect(hiddenElement.validationMessage).toBe(''); + }); + }); + }); + }); + + describe('v-model', () => { + beforeEach(() => { + wrapper = mount({ + data() { + return { value: 1 * 60 + 10 }; + }, + components: { ChronicDurationInput }, + template: '
', + }); + findComponents(); + }); + + describe('value', () => { + it('passes initial prop via v-model', () => { + expect(textElement.value).toBe('1 min 10 secs'); + expect(hiddenElement.value).toBe((1 * 60 + 10).toString()); + }); + + it('passes updated prop via v-model', async () => { + wrapper.setData({ value: MOCK_VALUE }); + await wrapper.vm.$nextTick(); + + expect(textElement.value).toBe('2 hrs 20 mins'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + }); + + describe('change', () => { + it('passes user input to parent via v-model', async () => { + textElement.value = '2hr20min'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE); + expect(textElement.value).toBe('2hr20min'); + expect(hiddenElement.value).toBe(MOCK_VALUE.toString()); + }); + }); + }); + + describe('name', () => { + beforeEach(() => { + createComponent({ name: 'myInput' }); + }); + + it('sets name of hidden field', () => { + expect(hiddenElement.name).toBe('myInput'); + }); + + it('does not set name of text field', () => { + expect(textElement.name).toBe(''); + }); + }); + + describe('form submission', () => { + beforeEach(() => { + wrapper = mount({ + template: `
`, + components: { + ChronicDurationInput, + }, + }); + findComponents(); + }); + + it('creates form data with initial value', () => { + const formData = new FormData(wrapper.find('[data-testid=myForm]').element); + const iter = formData.entries(); + + expect(iter.next()).toEqual({ + value: ['myInput', MOCK_VALUE.toString()], + done: false, + }); + expect(iter.next()).toEqual({ value: undefined, done: true }); + }); + + it('creates form data with user-specified value', async () => { + textElement.value = '1m10s'; + textElement.dispatchEvent(new Event('input')); + await wrapper.vm.$nextTick(); + + const formData = new FormData(wrapper.find('[data-testid=myForm]').element); + const iter = formData.entries(); + + expect(iter.next()).toEqual({ + value: ['myInput', (1 * 60 + 10).toString()], + done: false, + }); + expect(iter.next()).toEqual({ value: undefined, done: true }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 639ad1d9be6..bba38ed2e47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -924,10 +924,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== -"@gitlab/ui@32.38.0": - version "32.38.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.38.0.tgz#580bda8daacdf23c1f6a75d77f6df44b1dbe1726" - integrity sha512-BS0+4JicfuiCbaWTTok0dQUzUCI8m8t5T7//DQUQqqwCZLYeJlb1AxMatd6IjwcdE0m+AhST3iOZi2x+hDrkbQ== +"@gitlab/ui@32.39.0": + version "32.39.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.39.0.tgz#81dd3fd730721e828f62f6aff0a3f41afc3ac309" + integrity sha512-SgbmTkjpMFoTGREkJM7D7/VnzQjdmAikF45pqQJVJWZtGSLAadIbuZQBFUYwa9SFC6BhXjeoSmyRxBr1sdB19A== dependencies: "@babel/standalone" "^7.0.0" bootstrap-vue "2.20.1"