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');
+    });
+  });
+});