diff --git a/app/assets/javascripts/google_cloud/components/app.vue b/app/assets/javascripts/google_cloud/components/app.vue new file mode 100644 index 0000000000000000000000000000000000000000..1e5be9df019f544c4e71fd823d3689a382d71e16 --- /dev/null +++ b/app/assets/javascripts/google_cloud/components/app.vue @@ -0,0 +1,50 @@ +<script> +import { GlTab, GlTabs } from '@gitlab/ui'; +import IncubationBanner from './incubation_banner.vue'; +import ServiceAccounts from './service_accounts.vue'; + +export default { + components: { GlTab, GlTabs, IncubationBanner, ServiceAccounts }, + props: { + serviceAccounts: { + type: Array, + required: true, + }, + createServiceAccountUrl: { + type: String, + required: true, + }, + emptyIllustrationUrl: { + type: String, + required: true, + }, + }, + methods: { + feedbackUrl(template) { + return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`; + }, + }, +}; +</script> + +<template> + <div> + <incubation-banner + :share-feedback-url="feedbackUrl('general_feedback')" + :report-bug-url="feedbackUrl('report_bug')" + :feature-request-url="feedbackUrl('feature_request')" + /> + <gl-tabs> + <gl-tab :title="__('Configuration')"> + <service-accounts + class="gl-mx-3" + :list="serviceAccounts" + :create-url="createServiceAccountUrl" + :empty-illustration-url="emptyIllustrationUrl" + /> + </gl-tab> + <gl-tab :title="__('Deployments')" disabled /> + <gl-tab :title="__('Services')" disabled /> + </gl-tabs> + </div> +</template> diff --git a/app/assets/javascripts/google_cloud/components/incubation_banner.vue b/app/assets/javascripts/google_cloud/components/incubation_banner.vue new file mode 100644 index 0000000000000000000000000000000000000000..652b8c1aecbb03841324ace1a974a2440ce8da3a --- /dev/null +++ b/app/assets/javascripts/google_cloud/components/incubation_banner.vue @@ -0,0 +1,44 @@ +<script> +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; + +export default { + components: { GlAlert, GlLink, GlSprintf }, + props: { + shareFeedbackUrl: { + required: true, + type: String, + }, + reportBugUrl: { + required: true, + type: String, + }, + featureRequestUrl: { + required: true, + type: String, + }, + }, +}; +</script> + +<template> + <gl-alert :dismissible="false" variant="info"> + {{ __('This is an experimental feature developed by GitLab Incubation Engineering.') }} + <gl-sprintf + :message=" + __( + 'We invite you to %{featureLinkStart}request a feature%{featureLinkEnd}, %{bugLinkStart}report a bug%{bugLinkEnd} or %{feedbackLinkStart}share feedback%{feedbackLinkEnd}', + ) + " + > + <template #featureLink="{ content }"> + <gl-link :href="featureRequestUrl">{{ content }}</gl-link> + </template> + <template #bugLink="{ content }"> + <gl-link :href="reportBugUrl">{{ content }}</gl-link> + </template> + <template #feedbackLink="{ content }"> + <gl-link :href="shareFeedbackUrl">{{ content }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> +</template> diff --git a/app/assets/javascripts/google_cloud/components/service_accounts.vue b/app/assets/javascripts/google_cloud/components/service_accounts.vue new file mode 100644 index 0000000000000000000000000000000000000000..b70b25a5dc36301f4922f65387dd6e250e1b9369 --- /dev/null +++ b/app/assets/javascripts/google_cloud/components/service_accounts.vue @@ -0,0 +1,65 @@ +<script> +import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { GlButton, GlEmptyState, GlTable }, + props: { + list: { + type: Array, + required: true, + }, + createUrl: { + type: String, + required: true, + }, + emptyIllustrationUrl: { + type: String, + required: true, + }, + }, + data() { + return { + tableFields: [ + { key: 'environment', label: __('Environment'), sortable: true }, + { key: 'gcp_project', label: __('Google Cloud Project'), sortable: true }, + { key: 'service_account_exists', label: __('Service Account'), sortable: true }, + { key: 'service_account_key_exists', label: __('Service Account Key'), sortable: true }, + ], + }; + }, +}; +</script> + +<template> + <div> + <gl-empty-state + v-if="list.length === 0" + :title="__('No service accounts')" + :description=" + __('Service Accounts keys authorize GitLab to deploy your Google Cloud project') + " + :primary-button-link="createUrl" + :primary-button-text="__('Create service account')" + :svg-path="emptyIllustrationUrl" + /> + + <div v-else> + <h2 class="gl-font-size-h2">{{ __('Service Accounts') }}</h2> + <p>{{ __('Service Accounts keys authorize GitLab to deploy your Google Cloud project') }}</p> + + <gl-table :items="list" :fields="tableFields"> + <template #cell(service_account_exists)="{ value }"> + {{ value ? '✔' : __('Not found') }} + </template> + <template #cell(service_account_key_exists)="{ value }"> + {{ value ? '✔' : __('Not found') }} + </template> + </gl-table> + + <gl-button :href="createUrl" category="primary" variant="info"> + {{ __('Create service account') }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/google_cloud/index.js b/app/assets/javascripts/google_cloud/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a283860521975387c5900fdc926a38a6103aa15d --- /dev/null +++ b/app/assets/javascripts/google_cloud/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import App from './components/app.vue'; + +const elementRenderer = (element, props = {}) => (createElement) => + createElement(element, { props }); + +export default () => { + const root = document.querySelector('#js-google-cloud'); + + // uncomment this once backend is ready + // const dataset = JSON.parse(root.getAttribute('data')); + const mockDataset = { + createServiceAccountUrl: '#create-url', + serviceAccounts: [], + emptyIllustrationUrl: + 'https://gitlab.com/gitlab-org/gitlab-svgs/-/raw/main/illustrations/pipelines_empty.svg', + }; + return new Vue({ el: root, render: elementRenderer(App, mockDataset) }); +}; diff --git a/app/assets/javascripts/pages/projects/google_cloud/index.js b/app/assets/javascripts/pages/projects/google_cloud/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4506ea8efd1ffb461fba48fa29eeca9322b8573b --- /dev/null +++ b/app/assets/javascripts/pages/projects/google_cloud/index.js @@ -0,0 +1,3 @@ +import initGoogleCloud from '~/google_cloud/index'; + +initGoogleCloud(); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4eb9456aacad15a9e831d340d7f0c937a8d1914c..ca711e319673f14fec83309b1652745cc00f049f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9742,6 +9742,9 @@ msgstr "" msgid "Create requirement" msgstr "" +msgid "Create service account" +msgstr "" + msgid "Create snippet" msgstr "" @@ -16034,6 +16037,9 @@ msgstr "" msgid "Google Cloud" msgstr "" +msgid "Google Cloud Project" +msgstr "" + msgid "Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service." msgstr "" @@ -23369,6 +23375,9 @@ msgstr "" msgid "No schedules" msgstr "" +msgid "No service accounts" +msgstr "" + msgid "No severity matches the provided parameter" msgstr "" @@ -23464,6 +23473,9 @@ msgstr "" msgid "Not confidential" msgstr "" +msgid "Not found" +msgstr "" + msgid "Not found." msgstr "" @@ -31285,6 +31297,18 @@ msgstr "" msgid "Service" msgstr "" +msgid "Service Account" +msgstr "" + +msgid "Service Account Key" +msgstr "" + +msgid "Service Accounts" +msgstr "" + +msgid "Service Accounts keys authorize GitLab to deploy your Google Cloud project" +msgstr "" + msgid "Service Desk" msgstr "" @@ -31339,6 +31363,9 @@ msgstr "" msgid "ServicePing|Turn on service ping to review instance-level analytics." msgstr "" +msgid "Services" +msgstr "" + msgid "Session ID" msgstr "" @@ -35113,6 +35140,9 @@ msgstr "" msgid "This is a self-managed instance of GitLab." msgstr "" +msgid "This is an experimental feature developed by GitLab Incubation Engineering." +msgstr "" + msgid "This is the highest peak of users on your installation since the license started." msgstr "" @@ -38359,6 +38389,9 @@ msgstr "" msgid "We heard back from your device. You have been authenticated." msgstr "" +msgid "We invite you to %{featureLinkStart}request a feature%{featureLinkEnd}, %{bugLinkStart}report a bug%{bugLinkEnd} or %{feedbackLinkStart}share feedback%{feedbackLinkEnd}" +msgstr "" + msgid "We recommend cloud-based mobile authenticator apps such as Authy, Duo Mobile, and LastPass. They can restore access if you lose your hardware device." msgstr "" diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..bb86eb5c22e07643398ce2e715eef89b3fca7b49 --- /dev/null +++ b/spec/frontend/google_cloud/components/app_spec.js @@ -0,0 +1,66 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlTab, GlTabs } from '@gitlab/ui'; +import App from '~/google_cloud/components/app.vue'; +import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; +import ServiceAccounts from '~/google_cloud/components/service_accounts.vue'; + +describe('google_cloud App component', () => { + let wrapper; + + const findIncubationBanner = () => wrapper.findComponent(IncubationBanner); + const findTabs = () => wrapper.findComponent(GlTabs); + const findTabItems = () => findTabs().findAllComponents(GlTab); + const findConfigurationTab = () => findTabItems().at(0); + const findDeploymentTab = () => findTabItems().at(1); + const findServicesTab = () => findTabItems().at(2); + const findServiceAccounts = () => findConfigurationTab().findComponent(ServiceAccounts); + + beforeEach(() => { + const propsData = { + serviceAccounts: [{}, {}], + createServiceAccountUrl: '#url-create-service-account', + emptyIllustrationUrl: '#url-empty-illustration', + }; + wrapper = shallowMount(App, { propsData }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should contain incubation banner', () => { + expect(findIncubationBanner().exists()).toBe(true); + }); + + describe('google_cloud App tabs', () => { + it('should contain tabs', () => { + expect(findTabs().exists()).toBe(true); + }); + + it('should contain three tab items', () => { + expect(findTabItems().length).toBe(3); + }); + + describe('configuration tab', () => { + it('should exist', () => { + expect(findConfigurationTab().exists()).toBe(true); + }); + + it('should contain service accounts component', () => { + expect(findServiceAccounts().exists()).toBe(true); + }); + }); + + describe('deployments tab', () => { + it('should exist', () => { + expect(findDeploymentTab().exists()).toBe(true); + }); + }); + + describe('services tab', () => { + it('should exist', () => { + expect(findServicesTab().exists()).toBe(true); + }); + }); + }); +}); diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..89517be4ef165bf69223a5a97005cf798d8e8308 --- /dev/null +++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js @@ -0,0 +1,60 @@ +import { mount } from '@vue/test-utils'; +import { GlAlert, GlLink } from '@gitlab/ui'; +import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; + +describe('IncubationBanner component', () => { + let wrapper; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findLinks = () => wrapper.findAllComponents(GlLink); + const findFeatureRequestLink = () => findLinks().at(0); + const findReportBugLink = () => findLinks().at(1); + const findShareFeedbackLink = () => findLinks().at(2); + + beforeEach(() => { + const propsData = { + shareFeedbackUrl: 'url_general_feedback', + reportBugUrl: 'url_report_bug', + featureRequestUrl: 'url_feature_request', + }; + wrapper = mount(IncubationBanner, { propsData }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains alert', () => { + expect(findAlert().exists()).toBe(true); + }); + + it('contains relevant text', () => { + expect(findAlert().text()).toContain( + 'This is an experimental feature developed by GitLab Incubation Engineering.', + ); + }); + + describe('has relevant gl-links', () => { + it('three in total', () => { + expect(findLinks().length).toBe(3); + }); + + it('contains feature request link', () => { + const link = findFeatureRequestLink(); + expect(link.text()).toBe('request a feature'); + expect(link.attributes('href')).toBe('url_feature_request'); + }); + + it('contains report bug link', () => { + const link = findReportBugLink(); + expect(link.text()).toBe('report a bug'); + expect(link.attributes('href')).toBe('url_report_bug'); + }); + + it('contains share feedback link', () => { + const link = findShareFeedbackLink(); + expect(link.text()).toBe('share feedback'); + expect(link.attributes('href')).toBe('url_general_feedback'); + }); + }); +}); diff --git a/spec/frontend/google_cloud/components/service_accounts_spec.js b/spec/frontend/google_cloud/components/service_accounts_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..3d097078f0334c402e5f0a8cb9979bade6ba4d36 --- /dev/null +++ b/spec/frontend/google_cloud/components/service_accounts_spec.js @@ -0,0 +1,79 @@ +import { mount } from '@vue/test-utils'; +import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; +import ServiceAccounts from '~/google_cloud/components/service_accounts.vue'; + +describe('ServiceAccounts component', () => { + describe('when the project does not have any service accounts', () => { + let wrapper; + + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findButtonInEmptyState = () => findEmptyState().findComponent(GlButton); + + beforeEach(() => { + const propsData = { + list: [], + createUrl: '#create-url', + emptyIllustrationUrl: '#empty-illustration-url', + }; + wrapper = mount(ServiceAccounts, { propsData }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('shows the empty state component', () => { + expect(findEmptyState().exists()).toBe(true); + }); + it('shows the link to create new service accounts', () => { + const button = findButtonInEmptyState(); + expect(button.exists()).toBe(true); + expect(button.text()).toBe('Create service account'); + expect(button.attributes('href')).toBe('#create-url'); + }); + }); + + describe('when three service accounts are passed via props', () => { + let wrapper; + + const findTitle = () => wrapper.find('h2'); + const findDescription = () => wrapper.find('p'); + const findTable = () => wrapper.findComponent(GlTable); + const findRows = () => findTable().findAll('tr'); + const findButton = () => wrapper.findComponent(GlButton); + + beforeEach(() => { + const propsData = { + list: [{}, {}, {}], + createUrl: '#create-url', + emptyIllustrationUrl: '#empty-illustration-url', + }; + wrapper = mount(ServiceAccounts, { propsData }); + }); + + it('shows the title', () => { + expect(findTitle().text()).toBe('Service Accounts'); + }); + + it('shows the description', () => { + expect(findDescription().text()).toBe( + 'Service Accounts keys authorize GitLab to deploy your Google Cloud project', + ); + }); + + it('shows the table', () => { + expect(findTable().exists()).toBe(true); + }); + + it('table must have three rows + header row', () => { + expect(findRows().length).toBe(4); + }); + + it('shows the link to create new service accounts', () => { + const button = findButton(); + expect(button.exists()).toBe(true); + expect(button.text()).toBe('Create service account'); + expect(button.attributes('href')).toBe('#create-url'); + }); + }); +});