Skip to content
Snippets Groups Projects
Commit 253a17c5 authored by Robert Hunt's avatar Robert Hunt :two:
Browse files

Merge branch 'incubation-cloud-seed-cloudsql-frontend-components' into 'master'

Introduce CloudSQL Frontend Components

See merge request !90261
parents 027aa54b 01d7b9bf
No related branches found
No related tags found
1 merge request!90261Introduce CloudSQL Frontend Components
Pipeline #576927385 passed
<script>
import { GlButton, GlFormCheckbox, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { s__ } from '~/locale';
const i18n = {
gcpProjectLabel: s__('CloudSeed|Google Cloud project'),
gcpProjectDescription: s__(
'CloudSeed|Database instance is generated within the selected Google Cloud project',
),
refsLabel: s__('CloudSeed|Refs'),
refsDescription: s__(
'CloudSeed|Generated database instance is linked to the selected branch or tag',
),
databaseVersionLabel: s__('CloudSeed|Database version'),
tierLabel: s__('CloudSeed|Machine type'),
tierDescription: s__('CloudSeed|Determines memory and virtual cores available to your instance'),
checkboxLabel: s__(
'CloudSeed|I accept Google Cloud pricing and responsibilities involved with managing database instances',
),
cancelLabel: s__('CloudSeed|Cancel'),
submitLabel: s__('CloudSeed|Create instance'),
all: s__('CloudSeed|All'),
};
export default {
ALL_REFS: '*',
components: {
GlButton,
GlFormCheckbox,
GlFormGroup,
GlFormSelect,
},
props: {
cancelPath: { required: true, type: String },
gcpProjects: { required: true, type: Array },
refs: { required: true, type: Array },
formTitle: { required: true, type: String },
formDescription: { required: true, type: String },
databaseVersions: { required: true, type: Array },
tiers: { required: true, type: Array },
},
i18n,
};
</script>
<template>
<div>
<header class="gl-my-5 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid">
<h2 class="gl-font-size-h1">{{ formTitle }}</h2>
<p>{{ formDescription }}</p>
</header>
<gl-form-group
data-testid="form_group_gcp_project"
label-for="gcp_project"
:label="$options.i18n.gcpProjectLabel"
:description="$options.i18n.gcpProjectDescription"
>
<gl-form-select id="gcp_project" data-testid="select_gcp_project" name="gcp_project" required>
<option
v-for="gcpProject in gcpProjects"
:key="gcpProject.project_id"
:value="gcpProject.project_id"
>
{{ gcpProject.name }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
data-testid="form_group_environments"
label-for="ref"
:label="$options.i18n.refsLabel"
:description="$options.i18n.refsDescription"
>
<gl-form-select id="ref" data-testid="select_environments" name="ref" required>
<option :value="$options.ALL_REFS">{{ $options.i18n.all }}</option>
<option v-for="ref in refs" :key="ref" :value="ref">
{{ ref }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
data-testid="form_group_tier"
label-for="tier"
:label="$options.i18n.tierLabel"
:description="$options.i18n.tierDescription"
>
<gl-form-select id="tier" data-testid="select_tier" name="tier" required>
<option v-for="tier in tiers" :key="tier.value" :value="tier.value">
{{ tier.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
data-testid="form_group_database_version"
label-for="database-version"
:label="$options.i18n.databaseVersionLabel"
>
<gl-form-select
id="database-version"
data-testid="select_database_version"
name="database_version"
required
>
<option
v-for="databaseVersion in databaseVersions"
:key="databaseVersion.value"
:value="databaseVersion.value"
>
{{ databaseVersion.label }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group>
<gl-form-checkbox name="confirmation" required>
{{ $options.i18n.checkboxLabel }}
</gl-form-checkbox>
</gl-form-group>
<div class="form-actions row">
<gl-button type="submit" category="primary" variant="confirm" data-testid="submit-button">
{{ $options.i18n.submitLabel }}
</gl-button>
<gl-button class="gl-ml-1" :href="cancelPath" data-testid="cancel-button">{{
$options.i18n.cancelLabel
}}</gl-button>
</div>
</div>
</template>
<script>
import { GlEmptyState, GlLink, GlTable } from '@gitlab/ui';
import { encodeSaferUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
const i18n = {
noInstancesTitle: s__('CloudSeed|No instances'),
noInstancesDescription: s__('CloudSeed|There are no instances to display.'),
title: s__('CloudSeed|Instances'),
description: s__('CloudSeed|Database instances associated with this project'),
};
export default {
components: { GlEmptyState, GlLink, GlTable },
props: {
cloudsqlInstances: {
type: Array,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
},
computed: {
tableData() {
return this.cloudsqlInstances.filter((instance) => instance.instance_name);
},
},
methods: {
gcpProjectUrl(id) {
return setUrlParams({ project: id }, 'https://console.cloud.google.com/sql/instances');
},
instanceUrl(name, id) {
const saferName = encodeSaferUrl(name);
return setUrlParams(
{ project: id },
`https://console.cloud.google.com/sql/instances/${saferName}/overview`,
);
},
},
fields: [
{ key: 'ref', label: s__('CloudSeed|Environment') },
{ key: 'gcp_project', label: s__('CloudSeed|Google Cloud Project') },
{ key: 'instance_name', label: s__('CloudSeed|CloudSQL Instance') },
{ key: 'version', label: s__('CloudSeed|Version') },
],
i18n,
};
</script>
<template>
<div class="gl-mx-3">
<gl-empty-state
v-if="tableData.length === 0"
:title="$options.i18n.noInstancesTitle"
:description="$options.i18n.noInstancesDescription"
:svg-path="emptyIllustrationUrl"
/>
<div v-else>
<h2 class="gl-font-size-h2">{{ $options.i18n.title }}</h2>
<p>{{ $options.i18n.description }}</p>
<gl-table :fields="$options.fields" :items="tableData">
<template #cell(gcp_project)="{ value }">
<gl-link :href="gcpProjectUrl(value)">{{ value }}</gl-link>
</template>
<template #cell(instance_name)="{ item: { instance_name, gcp_project } }">
<a :href="instanceUrl(instance_name, gcp_project)">{{ instance_name }}</a>
</template>
</gl-table>
</div>
</div>
</template>
<script>
import { GlAlert, GlButton, GlLink, GlSprintf, GlTable } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
const KEY_CLOUDSQL_POSTGRES = 'cloudsql-postgres';
const KEY_CLOUDSQL_MYSQL = 'cloudsql-mysql';
const KEY_CLOUDSQL_SQLSERVER = 'cloudsql-sqlserver';
const KEY_ALLOYDB_POSTGRES = 'alloydb-postgres';
const KEY_MEMORYSTORE_REDIS = 'memorystore-redis';
const KEY_FIRESTORE = 'firestore';
const i18n = {
columnService: s__('CloudSeed|Service'),
columnDescription: s__('CloudSeed|Description'),
cloudsqlPostgresTitle: s__('CloudSeed|Cloud SQL for Postgres'),
cloudsqlPostgresDescription: s__(
'CloudSeed|Fully managed relational database service for PostgreSQL',
),
cloudsqlMysqlTitle: s__('CloudSeed|Cloud SQL for MySQL'),
cloudsqlMysqlDescription: s__('CloudSeed|Fully managed relational database service for MySQL'),
cloudsqlSqlserverTitle: s__('CloudSeed|Cloud SQL for SQL Server'),
cloudsqlSqlserverDescription: s__(
'CloudSeed|Fully managed relational database service for SQL Server',
),
alloydbPostgresTitle: s__('CloudSeed|AlloyDB for Postgres'),
alloydbPostgresDescription: s__(
'CloudSeed|Fully managed PostgreSQL-compatible service for high-demand workloads',
),
memorystoreRedisTitle: s__('CloudSeed|Memorystore for Redis'),
memorystoreRedisDescription: s__(
'CloudSeed|Scalable, secure, and highly available in-memory service for Redis',
),
firestoreTitle: s__('CloudSeed|Cloud Firestore'),
firestoreDescription: s__(
'CloudSeed|Flexible, scalable NoSQL cloud database for client- and server-side development',
),
createInstance: s__('CloudSeed|Create instance'),
createCluster: s__('CloudSeed|Create cluster'),
createDatabase: s__('CloudSeed|Create database'),
title: s__('CloudSeed|Services'),
description: s__('CloudSeed|Available database services through which instances may be created'),
pricingAlert: s__(
'CloudSeed|Learn more about pricing for %{cloudsqlPricingStart}Cloud SQL%{cloudsqlPricingEnd}, %{alloydbPricingStart}Alloy DB%{alloydbPricingEnd}, %{memorystorePricingStart}Memorystore%{memorystorePricingEnd} and %{firestorePricingStart}Firestore%{firestorePricingEnd}.',
),
secretManagersDescription: s__(
'CloudSeed|Enhance security by storing database variables in secret managers - learn more about %{docLinkStart}secret management with GitLab%{docLinkEnd}',
),
};
const helpUrlSecrets = helpPagePath('ee/ci/secrets');
export default {
components: { GlAlert, GlButton, GlLink, GlSprintf, GlTable },
props: {
cloudsqlPostgresUrl: {
type: String,
required: true,
},
cloudsqlMysqlUrl: {
type: String,
required: true,
},
cloudsqlSqlserverUrl: {
type: String,
required: true,
},
alloydbPostgresUrl: {
type: String,
required: true,
},
memorystoreRedisUrl: {
type: String,
required: true,
},
firestoreUrl: {
type: String,
required: true,
},
},
methods: {
actionUrl(key) {
switch (key) {
case KEY_CLOUDSQL_POSTGRES:
return this.cloudsqlPostgresUrl;
case KEY_CLOUDSQL_MYSQL:
return this.cloudsqlMysqlUrl;
case KEY_CLOUDSQL_SQLSERVER:
return this.cloudsqlSqlserverUrl;
case KEY_ALLOYDB_POSTGRES:
return this.alloydbPostgresUrl;
case KEY_MEMORYSTORE_REDIS:
return this.memorystoreRedisUrl;
case KEY_FIRESTORE:
return this.firestoreUrl;
default:
return '#';
}
},
},
fields: [
{ key: 'title', label: i18n.columnService },
{ key: 'description', label: i18n.columnDescription },
{ key: 'action', label: '' },
],
items: [
{
title: i18n.cloudsqlPostgresTitle,
description: i18n.cloudsqlPostgresDescription,
action: {
key: KEY_CLOUDSQL_POSTGRES,
title: i18n.createInstance,
testId: 'button-cloudsql-postgres',
},
},
{
title: i18n.cloudsqlMysqlTitle,
description: i18n.cloudsqlMysqlDescription,
action: {
disabled: false,
key: KEY_CLOUDSQL_MYSQL,
title: i18n.createInstance,
testId: 'button-cloudsql-mysql',
},
},
{
title: i18n.cloudsqlSqlserverTitle,
description: i18n.cloudsqlSqlserverDescription,
action: {
disabled: false,
key: KEY_CLOUDSQL_SQLSERVER,
title: i18n.createInstance,
testId: 'button-cloudsql-sqlserver',
},
},
{
title: i18n.alloydbPostgresTitle,
description: i18n.alloydbPostgresDescription,
action: {
disabled: true,
key: KEY_ALLOYDB_POSTGRES,
title: i18n.createCluster,
testId: 'button-alloydb-postgres',
},
},
{
title: i18n.memorystoreRedisTitle,
description: i18n.memorystoreRedisDescription,
action: {
disabled: true,
key: KEY_MEMORYSTORE_REDIS,
title: i18n.createInstance,
testId: 'button-memorystore-redis',
},
},
{
title: i18n.firestoreTitle,
description: i18n.firestoreDescription,
action: {
disabled: true,
key: KEY_FIRESTORE,
title: i18n.createDatabase,
testId: 'button-firestore',
},
},
],
helpUrlSecrets,
i18n,
};
</script>
<template>
<div class="gl-mx-3">
<h2 class="gl-font-size-h2">{{ $options.i18n.title }}</h2>
<p>{{ $options.i18n.description }}</p>
<gl-table :fields="$options.fields" :items="$options.items">
<template #cell(action)="{ value }">
<gl-button
block
:disabled="value.disabled"
:href="actionUrl(value.key)"
:data-testid="value.testId"
category="secondary"
variant="confirm"
>
{{ value.title }}
</gl-button>
</template>
</gl-table>
<gl-alert class="gl-mt-5" :dismissible="false" variant="tip">
<gl-sprintf :message="$options.i18n.pricingAlert">
<template #cloudsqlPricing="{ content }">
<gl-link href="https://cloud.google.com/sql/pricing">{{ content }}</gl-link>
</template>
<template #alloydbPricing="{ content }">
<gl-link href="https://cloud.google.com/alloydb/pricing">{{ content }}</gl-link>
</template>
<template #memorystorePricing="{ content }">
<gl-link href="https://cloud.google.com/memorystore/docs/redis/pricing">{{
content
}}</gl-link>
</template>
<template #firestorePricing="{ content }">
<gl-link href="https://cloud.google.com/firestore/pricing">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert class="gl-mt-5" :dismissible="false" variant="tip">
<gl-sprintf :message="$options.i18n.secretManagersDescription">
<template #docLink="{ content }">
<gl-link :href="$options.helpUrlSecrets">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</div>
</template>
......@@ -8255,6 +8255,123 @@ msgstr ""
msgid "Cloud Storage"
msgstr ""
 
msgid "CloudSeed|All"
msgstr ""
msgid "CloudSeed|AlloyDB for Postgres"
msgstr ""
msgid "CloudSeed|Available database services through which instances may be created"
msgstr ""
msgid "CloudSeed|Cancel"
msgstr ""
msgid "CloudSeed|Cloud Firestore"
msgstr ""
msgid "CloudSeed|Cloud SQL for MySQL"
msgstr ""
msgid "CloudSeed|Cloud SQL for Postgres"
msgstr ""
msgid "CloudSeed|Cloud SQL for SQL Server"
msgstr ""
msgid "CloudSeed|CloudSQL Instance"
msgstr ""
msgid "CloudSeed|Create cluster"
msgstr ""
msgid "CloudSeed|Create database"
msgstr ""
msgid "CloudSeed|Create instance"
msgstr ""
msgid "CloudSeed|Database instance is generated within the selected Google Cloud project"
msgstr ""
msgid "CloudSeed|Database instances associated with this project"
msgstr ""
msgid "CloudSeed|Database version"
msgstr ""
msgid "CloudSeed|Description"
msgstr ""
msgid "CloudSeed|Determines memory and virtual cores available to your instance"
msgstr ""
msgid "CloudSeed|Enhance security by storing database variables in secret managers - learn more about %{docLinkStart}secret management with GitLab%{docLinkEnd}"
msgstr ""
msgid "CloudSeed|Environment"
msgstr ""
msgid "CloudSeed|Flexible, scalable NoSQL cloud database for client- and server-side development"
msgstr ""
msgid "CloudSeed|Fully managed PostgreSQL-compatible service for high-demand workloads"
msgstr ""
msgid "CloudSeed|Fully managed relational database service for MySQL"
msgstr ""
msgid "CloudSeed|Fully managed relational database service for PostgreSQL"
msgstr ""
msgid "CloudSeed|Fully managed relational database service for SQL Server"
msgstr ""
msgid "CloudSeed|Generated database instance is linked to the selected branch or tag"
msgstr ""
msgid "CloudSeed|Google Cloud Project"
msgstr ""
msgid "CloudSeed|Google Cloud project"
msgstr ""
msgid "CloudSeed|I accept Google Cloud pricing and responsibilities involved with managing database instances"
msgstr ""
msgid "CloudSeed|Instances"
msgstr ""
msgid "CloudSeed|Learn more about pricing for %{cloudsqlPricingStart}Cloud SQL%{cloudsqlPricingEnd}, %{alloydbPricingStart}Alloy DB%{alloydbPricingEnd}, %{memorystorePricingStart}Memorystore%{memorystorePricingEnd} and %{firestorePricingStart}Firestore%{firestorePricingEnd}."
msgstr ""
msgid "CloudSeed|Machine type"
msgstr ""
msgid "CloudSeed|Memorystore for Redis"
msgstr ""
msgid "CloudSeed|No instances"
msgstr ""
msgid "CloudSeed|Refs"
msgstr ""
msgid "CloudSeed|Scalable, secure, and highly available in-memory service for Redis"
msgstr ""
msgid "CloudSeed|Service"
msgstr ""
msgid "CloudSeed|Services"
msgstr ""
msgid "CloudSeed|There are no instances to display."
msgstr ""
msgid "CloudSeed|Version"
msgstr ""
msgid "Cluster"
msgstr ""
 
import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InstanceForm from '~/google_cloud/components/cloudsql/create_instance_form.vue';
describe('google_cloud::cloudsql::create_instance_form component', () => {
let wrapper;
const findByTestId = (id) => wrapper.findByTestId(id);
const findCancelButton = () => findByTestId('cancel-button');
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findHeader = () => wrapper.find('header');
const findSubmitButton = () => findByTestId('submit-button');
const propsData = {
gcpProjects: [],
refs: [],
cancelPath: '#cancel-url',
formTitle: 'mock form title',
formDescription: 'mock form description',
databaseVersions: [],
tiers: [],
};
beforeEach(() => {
wrapper = shallowMountExtended(InstanceForm, { propsData, stubs: { GlFormCheckbox } });
});
afterEach(() => {
wrapper.destroy();
});
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
it('contains GCP project form group', () => {
const formGroup = findByTestId('form_group_gcp_project');
expect(formGroup.exists()).toBe(true);
expect(formGroup.attributes('label')).toBe(InstanceForm.i18n.gcpProjectLabel);
expect(formGroup.attributes('description')).toBe(InstanceForm.i18n.gcpProjectDescription);
});
it('contains GCP project dropdown', () => {
const select = findByTestId('select_gcp_project');
expect(select.exists()).toBe(true);
});
it('contains Environments form group', () => {
const formGroup = findByTestId('form_group_environments');
expect(formGroup.exists()).toBe(true);
expect(formGroup.attributes('label')).toBe(InstanceForm.i18n.refsLabel);
expect(formGroup.attributes('description')).toBe(InstanceForm.i18n.refsDescription);
});
it('contains Environments dropdown', () => {
const select = findByTestId('select_environments');
expect(select.exists()).toBe(true);
});
it('contains Tier form group', () => {
const formGroup = findByTestId('form_group_tier');
expect(formGroup.exists()).toBe(true);
expect(formGroup.attributes('label')).toBe(InstanceForm.i18n.tierLabel);
expect(formGroup.attributes('description')).toBe(InstanceForm.i18n.tierDescription);
});
it('contains Tier dropdown', () => {
const select = findByTestId('select_tier');
expect(select.exists()).toBe(true);
});
it('contains Database Version form group', () => {
const formGroup = findByTestId('form_group_database_version');
expect(formGroup.exists()).toBe(true);
expect(formGroup.attributes('label')).toBe(InstanceForm.i18n.databaseVersionLabel);
});
it('contains Database Version dropdown', () => {
const select = findByTestId('select_database_version');
expect(select.exists()).toBe(true);
});
it('contains Submit button', () => {
expect(findSubmitButton().exists()).toBe(true);
expect(findSubmitButton().text()).toBe(InstanceForm.i18n.submitLabel);
});
it('contains Cancel button', () => {
expect(findCancelButton().exists()).toBe(true);
expect(findCancelButton().text()).toBe(InstanceForm.i18n.cancelLabel);
expect(findCancelButton().attributes('href')).toBe('#cancel-url');
});
it('contains Confirmation checkbox', () => {
const checkbox = findCheckbox();
expect(checkbox.text()).toBe(InstanceForm.i18n.checkboxLabel);
});
it('checkbox must be required', () => {
const checkbox = findCheckbox();
expect(checkbox.attributes('required')).toBe('true');
});
});
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlTable } from '@gitlab/ui';
import InstanceTable from '~/google_cloud/components/cloudsql/instance_table.vue';
describe('google_cloud::databases::service_table component', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTable = () => wrapper.findComponent(GlTable);
afterEach(() => {
wrapper.destroy();
});
describe('when there are no instances', () => {
beforeEach(() => {
const propsData = {
cloudsqlInstances: [],
emptyIllustrationUrl: '#empty-illustration-url',
};
wrapper = shallowMount(InstanceTable, { propsData });
});
it('should depict empty state', () => {
const emptyState = findEmptyState();
expect(emptyState.exists()).toBe(true);
expect(emptyState.attributes('title')).toBe(InstanceTable.i18n.noInstancesTitle);
expect(emptyState.attributes('description')).toBe(InstanceTable.i18n.noInstancesDescription);
});
});
describe('when there are three instances', () => {
beforeEach(() => {
const propsData = {
cloudsqlInstances: [
{
ref: '*',
gcp_project: 'test-gcp-project',
instance_name: 'postgres-14-instance',
version: 'POSTGRES_14',
},
{
ref: 'production',
gcp_project: 'prod-gcp-project',
instance_name: 'postgres-14-instance',
version: 'POSTGRES_14',
},
{
ref: 'staging',
gcp_project: 'test-gcp-project',
instance_name: 'postgres-14-instance',
version: 'POSTGRES_14',
},
],
emptyIllustrationUrl: '#empty-illustration-url',
};
wrapper = shallowMount(InstanceTable, { propsData });
});
it('should contain a table', () => {
const table = findTable();
expect(table.exists()).toBe(true);
});
});
});
import { GlTable } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ServiceTable from '~/google_cloud/components/databases/service_table.vue';
describe('google_cloud::databases::service_table component', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
beforeEach(() => {
const propsData = {
cloudsqlPostgresUrl: '#url-cloudsql-postgres',
cloudsqlMysqlUrl: '#url-cloudsql-mysql',
cloudsqlSqlserverUrl: '#url-cloudsql-sqlserver',
alloydbPostgresUrl: '#url-alloydb-postgres',
memorystoreRedisUrl: '#url-memorystore-redis',
firestoreUrl: '#url-firestore',
};
wrapper = mountExtended(ServiceTable, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('should contain a table', () => {
expect(findTable().exists()).toBe(true);
});
it.each`
name | testId | url
${'cloudsql-postgres'} | ${'button-cloudsql-postgres'} | ${'#url-cloudsql-postgres'}
${'cloudsql-mysql'} | ${'button-cloudsql-mysql'} | ${'#url-cloudsql-mysql'}
${'cloudsql-sqlserver'} | ${'button-cloudsql-sqlserver'} | ${'#url-cloudsql-sqlserver'}
${'alloydb-postgres'} | ${'button-alloydb-postgres'} | ${'#url-alloydb-postgres'}
${'memorystore-redis'} | ${'button-memorystore-redis'} | ${'#url-memorystore-redis'}
${'firestore'} | ${'button-firestore'} | ${'#url-firestore'}
`('renders $name button with correct url', ({ testId, url }) => {
const button = wrapper.findByTestId(testId);
expect(button.exists()).toBe(true);
expect(button.attributes('href')).toBe(url);
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment