Commit b610767b authored by Sri's avatar Sri Committed by Paul Slaughter

Improvements to `Project::Infra::Google Cloud`

- Feature flag `incubation_5mp_google_cloud` bound to Project
  This allows for easier testing on gitlab.com where the feature
  flag may be enabled for select projects.

- `service_accounts` is an active route for `google_cloud` menu item
  This expands the appropriate project sidemenu item when creating
  a service account

- Introduce a top level `app.vue` component
  Achieves consistency with a majority of frontend code in the repo
  Where a single route with a single entry point serves multiple templates
  Instead of routing the frontend in `index.js` use a top level component
  to render the appropriate `screen`

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75295
parent 58a84211
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
import IncubationBanner from '../incubation_banner.vue';
import ServiceAccountsList from '../service_accounts_list.vue';
import { __ } from '~/locale';
import Home from './home.vue';
import IncubationBanner from './incubation_banner.vue';
import ServiceAccountsForm from './service_accounts_form.vue';
import NoGcpProjects from './errors/no_gcp_projects.vue';
import GcpError from './errors/gcp_error.vue';
const SCREEN_GCP_ERROR = 'gcp_error';
const SCREEN_HOME = 'home';
const SCREEN_NO_GCP_PROJECTS = 'no_gcp_projects';
const SCREEN_SERVICE_ACCOUNTS_FORM = 'service_accounts_form';
export default {
components: { GlTab, GlTabs, IncubationBanner, ServiceAccountsList },
components: {
IncubationBanner,
},
inheritAttrs: false,
props: {
serviceAccounts: {
type: Array,
screen: {
required: true,
},
createServiceAccountUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
computed: {
mainComponent() {
switch (this.screen) {
case SCREEN_HOME:
return Home;
case SCREEN_GCP_ERROR:
return GcpError;
case SCREEN_NO_GCP_PROJECTS:
return NoGcpProjects;
case SCREEN_SERVICE_ACCOUNTS_FORM:
return ServiceAccountsForm;
default:
throw new Error(__('Unknown screen'));
}
},
},
methods: {
......@@ -34,17 +54,6 @@ export default {
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
<gl-tabs>
<gl-tab :title="__('Configuration')">
<service-accounts-list
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>
<component :is="mainComponent" v-bind="$attrs" />
</div>
</template>
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import ServiceAccountsList from './service_accounts_list.vue';
export default {
components: {
GlTabs,
GlTab,
ServiceAccountsList,
},
props: {
serviceAccounts: {
type: Array,
required: true,
},
createServiceAccountUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-tabs>
<gl-tab :title="__('Configuration')">
<service-accounts-list
class="gl-mx-4"
:list="serviceAccounts"
:create-url="createServiceAccountUrl"
:empty-illustration-url="emptyIllustrationUrl"
/>
</gl-tab>
<gl-tab :title="__('Deployments')" disabled />
<gl-tab :title="__('Services')" disabled />
</gl-tabs>
</template>
<script>
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { __ } from '~/locale';
import IncubationBanner from '../incubation_banner.vue';
export default {
components: { GlButton, GlFormGroup, GlFormSelect, IncubationBanner },
components: { GlButton, GlFormGroup, GlFormSelect },
props: {
gcpProjects: { required: true, type: Array },
environments: { required: true, type: Array },
cancelPath: { required: true, type: String },
},
methods: {
feedbackUrl(template) {
return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new?issuable_template=${template}`;
},
},
i18n: {
title: __('Create service account'),
gcpProjectLabel: __('Google Cloud project'),
......@@ -31,11 +25,6 @@ export default {
<template>
<div>
<incubation-banner
:share-feedback-url="feedbackUrl('general_feedback')"
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
<header class="gl-my-5 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid">
<h2 class="gl-font-size-h1">{{ $options.i18n.title }}</h2>
</header>
......
import Vue from 'vue';
import { __ } from '~/locale';
import App from './components/screens/app.vue';
import ServiceAccountsForm from './components/screens/service_accounts_form.vue';
import ErrorNoGcpProjects from './components/errors/no_gcp_projects.vue';
import ErrorGcpError from './components/errors/gcp_error.vue';
const elementRenderer = (element, props = {}) => (createElement) =>
createElement(element, { props });
const rootComponentMap = [
{
root: '#js-google-cloud-error-no-gcp-projects',
component: ErrorNoGcpProjects,
},
{
root: '#js-google-cloud-error-gcp-error',
component: ErrorGcpError,
},
{
root: '#js-google-cloud-service-accounts',
component: ServiceAccountsForm,
},
{
root: '#js-google-cloud',
component: App,
},
];
import App from './components/app.vue';
export default () => {
for (let i = 0; i < rootComponentMap.length; i += 1) {
const { root, component } = rootComponentMap[i];
const element = document.querySelector(root);
if (element) {
const props = JSON.parse(element.getAttribute('data'));
return new Vue({ el: root, render: elementRenderer(component, props) });
}
}
throw new Error(__('Unknown root'));
const root = '#js-google-cloud';
const element = document.querySelector(root);
const { screen, ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(App, { props: { screen }, attrs }),
});
};
......@@ -21,6 +21,6 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
end
def feature_flag_enabled!
access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud)
access_denied! unless Feature.enabled?(:incubation_5mp_google_cloud, project)
end
end
......@@ -9,10 +9,11 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
gcp_projects = google_api_client.list_projects
if gcp_projects.empty?
@js_data = {}.to_json
@js_data = { screen: 'no_gcp_projects' }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/no_gcp_projects'
else
@js_data = {
screen: 'service_accounts_form',
gcpProjects: gcp_projects,
environments: project.environments,
cancelPath: project_google_cloud_index_path(project)
......@@ -78,7 +79,7 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
def handle_gcp_error(error, project)
Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
@js_data = { error: error.to_s }.to_json
@js_data = { screen: 'gcp_error', error: error.to_s }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
end
end
......@@ -3,6 +3,7 @@
class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
def index
@js_data = {
screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg')
......
......@@ -3,4 +3,4 @@
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-error-gcp-error{ data: @js_data }
#js-google-cloud{ data: @js_data }
......@@ -3,4 +3,4 @@
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-error-no-gcp-projects{ data: @js_data }
#js-google-cloud{ data: @js_data }
......@@ -5,4 +5,4 @@
- @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_service_accounts_path(@project), method: 'post' do
#js-google-cloud-service-accounts{ data: @js_data }
#js-google-cloud{ data: @js_data }
......@@ -90,7 +90,7 @@ module Sidebars
end
def google_cloud_menu_item
feature_is_enabled = Feature.enabled?(:incubation_5mp_google_cloud)
feature_is_enabled = Feature.enabled?(:incubation_5mp_google_cloud, context.project)
user_has_permissions = can?(context.current_user, :admin_project_google_cloud, context.project)
unless feature_is_enabled && user_has_permissions
......@@ -100,7 +100,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project),
active_routes: { controller: :google_cloud },
active_routes: { controller: [:google_cloud, :service_accounts] },
item_id: :google_cloud
)
end
......
......@@ -37190,7 +37190,7 @@ msgstr ""
msgid "Unknown response text"
msgstr ""
msgid "Unknown root"
msgid "Unknown screen"
msgstr ""
msgid "Unknown user"
......
import { shallowMount } from '@vue/test-utils';
import App from '~/google_cloud/components/app.vue';
import Home from '~/google_cloud/components/home.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
const BASE_FEEDBACK_URL =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/meta/-/issues/new';
describe('google_cloud App component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
const findGcpError = () => wrapper.findComponent(GcpError);
const findNoGcpProjects = () => wrapper.findComponent(NoGcpProjects);
const findServiceAccountsForm = () => wrapper.findComponent(ServiceAccountsForm);
const findHome = () => wrapper.findComponent(Home);
afterEach(() => {
wrapper.destroy();
});
describe('for gcp_error screen', () => {
beforeEach(() => {
const propsData = {
screen: 'gcp_error',
error: 'mock_gcp_client_error',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the gcp_error screen', () => {
expect(findGcpError().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for no_gcp_projects screen', () => {
beforeEach(() => {
const propsData = {
screen: 'no_gcp_projects',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the no_gcp_projects screen', () => {
expect(findNoGcpProjects().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for service_accounts_form screen', () => {
beforeEach(() => {
const propsData = {
screen: 'service_accounts_form',
gcpProjects: [1, 2, 3],
environments: [4, 5, 6],
cancelPath: '',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the service_accounts_form screen', () => {
expect(findServiceAccountsForm().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
describe('for home screen', () => {
beforeEach(() => {
const propsData = {
screen: 'home',
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
wrapper = shallowMount(App, { propsData });
});
it('renders the home screen', () => {
expect(findHome().exists()).toBe(true);
});
it('should contain incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlTab, GlTabs } from '@gitlab/ui';
import App from '~/google_cloud/components/screens/app.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import Home from '~/google_cloud/components/home.vue';
import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
describe('google_cloud App component', () => {
describe('google_cloud Home 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 findServiceAccountsList = () => findConfigurationTab().findComponent(ServiceAccountsList);
const findTabItemsModel = () =>
findTabs()
.findAllComponents(GlTab)
.wrappers.map((x) => ({
title: x.attributes('title'),
disabled: x.attributes('disabled'),
}));
const TEST_HOME_PROPS = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
};
beforeEach(() => {
const propsData = {
serviceAccounts: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
emptyIllustrationUrl: '#url-empty-illustration',
screen: 'home',
...TEST_HOME_PROPS,
};
wrapper = shallowMount(App, { propsData });
wrapper = shallowMount(Home, { 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);
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
{ title: 'Deployments', disabled: '' },
{ title: 'Services', disabled: '' },
]);
});
describe('configuration tab', () => {
it('should exist', () => {
expect(findConfigurationTab().exists()).toBe(true);
});
it('should contain service accounts component', () => {
expect(findServiceAccountsList().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);
const serviceAccounts = findTabItems().at(0).findComponent(ServiceAccountsList);
expect(serviceAccounts.props()).toEqual({
list: TEST_HOME_PROPS.serviceAccounts,
createUrl: TEST_HOME_PROPS.createServiceAccountUrl,
emptyIllustrationUrl: TEST_HOME_PROPS.emptyIllustrationUrl,
});
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import ServiceAccountsForm from '~/google_cloud/components/screens/service_accounts_form.vue';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
describe('ServiceAccountsForm component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
const findHeader = () => wrapper.find('header');
const findAllFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findAllFormSelects = () => wrapper.findAllComponents(GlFormSelect);
......@@ -22,10 +20,6 @@ describe('ServiceAccountsForm component', () => {
wrapper.destroy();
});
it('contains incubation banner', () => {
expect(findIncubationBanner().exists()).toBe(true);
});
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
......
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