diff options
20 files changed, 453 insertions, 88 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index 95cbc602d5b..70a71baa590 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -8,7 +8,6 @@ globals: IS_EE: false plugins: - import - - html - "@gitlab/i18n" - "@gitlab/vue-i18n" settings: diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue new file mode 100644 index 00000000000..f9465da6fda --- /dev/null +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue @@ -0,0 +1,182 @@ +<script> +import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; +import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; +import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; + +export default { + components: { + DropdownButton, + DropdownSearchInput, + DropdownHiddenInput, + }, + props: { + fieldName: { + type: String, + required: false, + default: '', + }, + placeholder: { + type: String, + required: false, + default: '', + }, + defaultValue: { + type: String, + required: false, + default: '', + }, + value: { + type: Object, + required: false, + default: () => null, + }, + labelProperty: { + type: String, + required: false, + default: 'name', + }, + valueProperty: { + type: String, + required: false, + default: 'value', + }, + items: { + type: Array, + required: false, + default: () => [], + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + loadingText: { + type: String, + required: false, + default: '', + }, + disabledText: { + type: String, + required: false, + default: '', + }, + hasErrors: { + type: Boolean, + required: false, + default: false, + }, + errorMessage: { + type: String, + required: false, + default: '', + }, + searchFieldPlaceholder: { + type: String, + required: false, + default: '', + }, + emptyText: { + type: String, + required: false, + default: '', + }, + searchFn: { + type: Function, + required: false, + default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1, + }, + }, + data() { + return { + searchQuery: '', + selectedItem: null, + }; + }, + computed: { + toggleText() { + if (this.loading && this.loadingText) { + return this.loadingText; + } + + if (this.disabled && this.disabledText) { + return this.disabledText; + } + + if (!this.selectedItem) { + return this.placeholder; + } + + return this.selectedItemLabel; + }, + results() { + if (!this.items) { + return []; + } + + return this.items.filter(this.searchFn(this.searchQuery)); + }, + selectedItemLabel() { + return this.selectedItem && this.selectedItem[this.labelProperty]; + }, + selectedItemValue() { + return (this.selectedItem && this.selectedItem[this.valueProperty]) || ''; + }, + }, + methods: { + select(item) { + this.selectedItem = item; + this.$emit('input', item); + }, + }, +}; +</script> + +<template> + <div> + <div class="js-gcp-machine-type-dropdown dropdown"> + <dropdown-hidden-input :name="fieldName" :value="selectedItemValue" /> + <dropdown-button + :class="{ 'border-danger': hasErrors }" + :is-disabled="disabled" + :is-loading="loading" + :toggle-text="toggleText" + /> + <div class="dropdown-menu dropdown-select"> + <dropdown-search-input v-model="searchQuery" :placeholder-text="searchFieldPlaceholder" /> + <div class="dropdown-content"> + <ul> + <li v-if="!results.length"> + <span class="js-empty-text menu-item"> + {{ emptyText }} + </span> + </li> + <li v-for="item in results" :key="item.id"> + <button class="js-dropdown-item" type="button" @click.prevent="select(item)"> + <slot name="item" :item="item"> + {{ item.name }} + </slot> + </button> + </li> + </ul> + </div> + </div> + </div> + <span + v-if="hasErrors && errorMessage" + :class="[ + 'form-text js-eks-dropdown-error-message', + { + 'text-danger': hasErrors, + 'text-muted': !hasErrors, + }, + ]" + > + {{ errorMessage }} + </span> + </div> +</template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index 1ec45c8b651..6e74963dcb0 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -14,5 +14,12 @@ export default { }; </script> <template> - <form name="eks-cluster-configuration-form"></form> + <form name="eks-cluster-configuration-form"> + <div class="form-group"> + <label class="label-bold" name="role" for="eks-role"> + {{ s__('ClusterIntegration|Role name') }} + </label> + <role-name-dropdown /> + </div> + </form> </template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue index e69de29bb2d..70230b294ac 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/role_name_dropdown.vue @@ -0,0 +1,53 @@ +<script> +import { sprintf, s__ } from '~/locale'; + +import ClusterFormDropdown from './cluster_form_dropdown.vue'; + +export default { + components: { + ClusterFormDropdown, + }, + props: { + roles: { + type: Array, + required: false, + default: () => [], + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + helpText() { + return sprintf( + s__( + 'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}.', + ), + { + startLink: + '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">', + endLink: '</a>', + }, + false, + ); + }, + }, +}; +</script> +<template> + <div> + <cluster-form-dropdown + field-id="eks-role-name" + field-name="eks-role-name" + :items="roles" + :loading="loading" + :loading-text="s__('ClusterIntegration|Loading IAM Roles')" + :placeholder="s__('ClusterIntergation|Select role name')" + :search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')" + :empty-text="s__('ClusterIntegration|No IAM Roles found')" + /> + <p class="form-text text-muted" v-html="helpText"></p> + </div> +</template> diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 84f0900d9c1..104c68919f0 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -49,12 +49,12 @@ = render_if_exists 'projects/mirrors/table_pull_row' - @project.remote_mirrors.each_with_index do |mirror, index| - next if mirror.new_record? - %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } - %td.qa-mirror-repository-url= mirror.safe_url || _('Invalid URL') + %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row' } } + %td{ data: { qa_selector: 'mirror_repository_url_cell' } }= mirror.safe_url || _('Invalid URL') %td= _('Push') %td = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') - %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td{ data: { qa_selector: 'mirror_last_update_at_cell' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td - if mirror.disabled? = render 'projects/mirrors/disabled_mirror_badge' diff --git a/changelogs/unreleased/56883-migration.yml b/changelogs/unreleased/56883-migration.yml deleted file mode 100644 index d6eb49e62e1..00000000000 --- a/changelogs/unreleased/56883-migration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Create a project for self-monitoring the GitLab instance -merge_request: 31389 -author: -type: added diff --git a/db/fixtures/development/98_gitlab_instance_administration_project.rb b/db/fixtures/development/98_gitlab_instance_administration_project.rb deleted file mode 100644 index 6cf3ae95cee..00000000000 --- a/db/fixtures/development/98_gitlab_instance_administration_project.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute! diff --git a/db/fixtures/production/998_gitlab_instance_administration_project.rb b/db/fixtures/production/998_gitlab_instance_administration_project.rb deleted file mode 100644 index 6cf3ae95cee..00000000000 --- a/db/fixtures/production/998_gitlab_instance_administration_project.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute! diff --git a/db/post_migrate/20190801072937_add_gitlab_instance_administration_project.rb b/db/post_migrate/20190801072937_add_gitlab_instance_administration_project.rb deleted file mode 100644 index 580653f1da5..00000000000 --- a/db/post_migrate/20190801072937_add_gitlab_instance_administration_project.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class AddGitlabInstanceAdministrationProject < ActiveRecord::Migration[5.2] - DOWNTIME = false - - disable_ddl_transaction! - - def up - BackgroundMigrationWorker.perform_async('AddGitlabInstanceAdministrationProject', []) - end - - def down - ApplicationSetting.current_without_cache - &.instance_administration_project - &.owner - &.destroy! - end -end diff --git a/doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.png b/doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.png Binary files differdeleted file mode 100644 index a0874f66eaa..00000000000 --- a/doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.png +++ /dev/null diff --git a/doc/ci/introduction/img/gitlab_workflow_example_extended_v12_3.png b/doc/ci/introduction/img/gitlab_workflow_example_extended_v12_3.png Binary files differnew file mode 100644 index 00000000000..6e1066d4868 --- /dev/null +++ b/doc/ci/introduction/img/gitlab_workflow_example_extended_v12_3.png diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md index 366aca3442e..d5ba94f4d19 100644 --- a/doc/ci/introduction/index.md +++ b/doc/ci/introduction/index.md @@ -174,7 +174,7 @@ If we take a deeper look into the basic workflow, we can see the features available in GitLab at each stage of the DevOps lifecycle, as shown on the illustration below. -![Deeper look into the basic CI/CD workflow](img/gitlab_workflow_example_extended_11_11.png) +![Deeper look into the basic CI/CD workflow](img/gitlab_workflow_example_extended_v12_3.png) If you look at the image from the left to the right, you'll see some of the features available in GitLab diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 23882383200..77231c350f9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3514,6 +3514,9 @@ msgstr "" msgid "ClusterIntegration|Let's Encrypt" msgstr "" +msgid "ClusterIntegration|Loading IAM Roles" +msgstr "" + msgid "ClusterIntegration|Machine type" msgstr "" @@ -3523,6 +3526,9 @@ msgstr "" msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}" msgstr "" +msgid "ClusterIntegration|No IAM Roles found" +msgstr "" + msgid "ClusterIntegration|No machine types matched your search" msgstr "" @@ -3589,9 +3595,15 @@ msgstr "" msgid "ClusterIntegration|Request to begin uninstalling failed" msgstr "" +msgid "ClusterIntegration|Role name" +msgstr "" + msgid "ClusterIntegration|Save changes" msgstr "" +msgid "ClusterIntegration|Search IAM Roles" +msgstr "" + msgid "ClusterIntegration|Search machine types" msgstr "" @@ -3616,6 +3628,9 @@ msgstr "" msgid "ClusterIntegration|Select project to choose zone" msgstr "" +msgid "ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}." +msgstr "" + msgid "ClusterIntegration|Select zone" msgstr "" @@ -3739,6 +3754,9 @@ msgstr "" msgid "ClusterIntegration|sign up" msgstr "" +msgid "ClusterIntergation|Select role name" +msgstr "" + msgid "Code" msgstr "" diff --git a/package.json b/package.json index 864d13d4980..181f2c8f1a5 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,6 @@ "eslint": "~5.9.0", "eslint-import-resolver-jest": "^2.1.1", "eslint-import-resolver-webpack": "^0.10.1", - "eslint-plugin-html": "5.0.0", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jasmine": "^2.10.1", "eslint-plugin-jest": "^22.3.0", diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb index e3afaceda80..441235afca8 100644 --- a/qa/qa/page/project/settings/mirroring_repositories.rb +++ b/qa/qa/page/project/settings/mirroring_repositories.rb @@ -13,8 +13,8 @@ module QA view 'app/views/projects/mirrors/_mirror_repos.html.haml' do element :mirror_repository_url_input element :mirror_repository_button - element :mirror_repository_url - element :mirror_last_update_at + element :mirror_repository_url_cell + element :mirror_last_update_at_cell element :mirrored_repository_row end @@ -64,21 +64,21 @@ module QA wait(interval: 1) do within_element_by_index(:mirrored_repository_row, row_index) do - last_update = find_element(:mirror_last_update_at, wait: 0) + last_update = find_element(:mirror_last_update_at_cell, wait: 0) last_update.has_text?('just now') || last_update.has_text?('seconds') end end # Fail early if the page still shows that there has been no update within_element_by_index(:mirrored_repository_row, row_index) do - find_element(:mirror_last_update_at, wait: 0).assert_no_text('Never') + find_element(:mirror_last_update_at_cell, wait: 0).assert_no_text('Never') end end private def find_repository_row_index(target_url) - all_elements(:mirror_repository_url).index do |url| + all_elements(:mirror_repository_url_cell).index do |url| # The url might be a sanitized url but the target_url won't be so # we compare just the paths instead of the full url URI.parse(url.text).path == target_url.path diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index c84ade3a140..f79bb035c46 100644 --- a/qa/qa/resource/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -4,11 +4,12 @@ module QA module Resource module Repository class ProjectPush < Repository::Push + attr_accessor :project_name attr_writer :wait_for_push attribute :project do Project.fabricate! do |resource| - resource.name = 'project-with-code' + resource.name = project_name resource.description = 'Project with repository' end end @@ -19,6 +20,7 @@ module QA @commit_message = "This is a test commit" @branch_name = 'master' @new_branch = true + @project_name = 'project-with-code' @wait_for_push = true end diff --git a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js new file mode 100644 index 00000000000..e873ef0b2fa --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js @@ -0,0 +1,137 @@ +import { shallowMount } from '@vue/test-utils'; + +import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue'; +import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; +import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; +import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; + +describe('ClusterFormDropdown', () => { + let vm; + + beforeEach(() => { + vm = shallowMount(ClusterFormDropdown); + }); + afterEach(() => vm.destroy()); + + describe('when no item is selected', () => { + it('displays placeholder text', () => { + const placeholder = 'placeholder'; + + vm.setProps({ placeholder }); + + expect(vm.find(DropdownButton).props('toggleText')).toEqual(placeholder); + }); + }); + + describe('when an item is selected', () => { + const selectedItem = { name: 'Name', value: 'value' }; + + beforeEach(() => { + vm.setData({ selectedItem }); + }); + + it('displays selected item label', () => { + expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem.name); + }); + + it('sets selected value to dropdown hidden input', () => { + expect(vm.find(DropdownHiddenInput).props('value')).toEqual(selectedItem.value); + }); + }); + + describe('when an item is selected and has a custom label property', () => { + it('displays selected item custom label', () => { + const labelProperty = 'customLabel'; + const selectedItem = { [labelProperty]: 'Name' }; + + vm.setProps({ labelProperty }); + vm.setData({ selectedItem }); + + expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem[labelProperty]); + }); + }); + + describe('when loading', () => { + it('dropdown button isLoading', () => { + vm.setProps({ loading: true }); + + expect(vm.find(DropdownButton).props('isLoading')).toBe(true); + }); + }); + + describe('when loading and loadingText is provided', () => { + it('uses loading text as toggle button text', () => { + const loadingText = 'loading text'; + + vm.setProps({ loading: true, loadingText }); + + expect(vm.find(DropdownButton).props('toggleText')).toEqual(loadingText); + }); + }); + + describe('when disabled', () => { + it('dropdown button isDisabled', () => { + vm.setProps({ disabled: true }); + + expect(vm.find(DropdownButton).props('isDisabled')).toBe(true); + }); + }); + + describe('when disabled and disabledText is provided', () => { + it('uses disabled text as toggle button text', () => { + const disabledText = 'disabled text'; + + vm.setProps({ disabled: true, disabledText }); + + expect(vm.find(DropdownButton).props('toggleText')).toBe(disabledText); + }); + }); + + describe('when has errors', () => { + it('sets border-danger class selector to dropdown toggle', () => { + vm.setProps({ hasErrors: true }); + + expect(vm.find(DropdownButton).classes('border-danger')).toBe(true); + }); + }); + + describe('when has errors and an error message', () => { + it('displays error message', () => { + const errorMessage = 'error message'; + + vm.setProps({ hasErrors: true, errorMessage }); + + expect(vm.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage); + }); + }); + + describe('when no results are available', () => { + it('displays empty text', () => { + const emptyText = 'error message'; + + vm.setProps({ items: [], emptyText }); + + expect(vm.find('.js-empty-text').text()).toEqual(emptyText); + }); + }); + + it('displays search field placeholder', () => { + const searchFieldPlaceholder = 'Placeholder'; + + vm.setProps({ searchFieldPlaceholder }); + + expect(vm.find(DropdownSearchInput).props('placeholderText')).toEqual(searchFieldPlaceholder); + }); + + it('it filters results by search query', () => { + const secondItem = { name: 'second item' }; + const items = [{ name: 'first item' }, secondItem]; + const searchQuery = 'second'; + + vm.setProps({ items }); + vm.setData({ searchQuery }); + + expect(vm.findAll('.js-dropdown-item').length).toEqual(1); + expect(vm.find('.js-dropdown-item').text()).toEqual(secondItem.name); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js new file mode 100644 index 00000000000..657637c1b56 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/role_name_dropdown_spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; + +import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue'; +import RoleNameDropdown from '~/create_cluster/eks_cluster/components/role_name_dropdown.vue'; + +describe('RoleNameDropdown', () => { + let vm; + + beforeEach(() => { + vm = shallowMount(RoleNameDropdown); + }); + afterEach(() => vm.destroy()); + + it('renders a cluster-form-dropdown', () => { + expect(vm.find(ClusterFormDropdown).exists()).toBe(true); + }); + + it('sets roles to cluster-form-dropdown items property', () => { + const roles = [{ name: 'basic' }]; + + vm.setProps({ roles }); + + expect(vm.find(ClusterFormDropdown).props('items')).toEqual(roles); + }); + + it('sets a loading text', () => { + expect(vm.find(ClusterFormDropdown).props('loadingText')).toEqual('Loading IAM Roles'); + }); + + it('sets a placeholder', () => { + expect(vm.find(ClusterFormDropdown).props('placeholder')).toEqual('Select role name'); + }); + + it('sets an empty results text', () => { + expect(vm.find(ClusterFormDropdown).props('emptyText')).toEqual('No IAM Roles found'); + }); + + it('sets a search field placeholder', () => { + expect(vm.find(ClusterFormDropdown).props('searchFieldPlaceholder')).toEqual( + 'Search IAM Roles', + ); + }); +}); diff --git a/spec/migrations/add_gitlab_instance_administration_project_spec.rb b/spec/migrations/add_gitlab_instance_administration_project_spec.rb deleted file mode 100644 index 58fbba9836d..00000000000 --- a/spec/migrations/add_gitlab_instance_administration_project_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20190801072937_add_gitlab_instance_administration_project.rb') - -describe AddGitlabInstanceAdministrationProject, :migration do - let(:application_settings) { table(:application_settings) } - let(:users) { table(:users) } - - let(:prometheus_settings) do - { - enable: true, - listen_address: 'localhost:9090' - } - end - - before do - stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - - stub_config(prometheus: prometheus_settings) - end - - describe 'down' do - let!(:application_setting) { application_settings.create! } - let!(:user) { users.create!(admin: true, email: 'admin1@example.com', projects_limit: 10, state: :active) } - - it 'deletes group and project' do - migrate! - - expect(Project.count).to eq(1) - expect(Group.count).to eq(1) - - schema_migrate_down! - - expect(Project.count).to eq(0) - expect(Group.count).to eq(0) - end - end -end diff --git a/yarn.lock b/yarn.lock index aa3dea56560..59e4703e73d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4630,13 +4630,6 @@ eslint-plugin-filenames@^1.3.2: lodash.snakecase "4.1.1" lodash.upperfirst "4.3.1" -eslint-plugin-html@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-5.0.0.tgz#396e30a60dedee0122fe08f11d13c5ab22f20d32" - integrity sha512-f7p/7YQdgQUFVAX3nB4dnMQbrDeTalcA01PDhuvTLk0ZadCwM4Pb+639SRuqEf1zMkIxckLY+ckCr0hVP5zl6A== - dependencies: - htmlparser2 "^3.10.0" - eslint-plugin-import@^2.14.0, eslint-plugin-import@^2.16.0: version "2.16.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f" |