Commit 7cd1f8d5 authored by David O'Regan's avatar David O'Regan

Merge branch '323418-verify-and-validate-your-terraform-modules-in-the-ui' into 'master'

Customise Terraform modules details page

See merge request gitlab-org/gitlab!63770
parents c109716d afdfa282
...@@ -24,7 +24,6 @@ import DependencyRow from './dependency_row.vue'; ...@@ -24,7 +24,6 @@ import DependencyRow from './dependency_row.vue';
import InstallationCommands from './installation_commands.vue'; import InstallationCommands from './installation_commands.vue';
import PackageFiles from './package_files.vue'; import PackageFiles from './package_files.vue';
import PackageHistory from './package_history.vue'; import PackageHistory from './package_history.vue';
import PackageTitle from './package_title.vue';
export default { export default {
name: 'PackagesApp', name: 'PackagesApp',
...@@ -36,7 +35,9 @@ export default { ...@@ -36,7 +35,9 @@ export default {
GlTab, GlTab,
GlTabs, GlTabs,
GlSprintf, GlSprintf,
PackageTitle, PackageTitle: () => import('./package_title.vue'),
TerraformTitle: () =>
import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'),
PackagesListLoader, PackagesListLoader,
PackageListRow, PackageListRow,
DependencyRow, DependencyRow,
...@@ -50,6 +51,12 @@ export default { ...@@ -50,6 +51,12 @@ export default {
GlModal: GlModalDirective, GlModal: GlModalDirective,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
inject: {
titleComponent: {
default: 'PackageTitle',
from: 'titleComponent',
},
},
trackingActions: { ...TrackingActions }, trackingActions: { ...TrackingActions },
data() { data() {
return { return {
...@@ -160,7 +167,7 @@ export default { ...@@ -160,7 +167,7 @@ export default {
/> />
<div v-else class="packages-app"> <div v-else class="packages-app">
<package-title> <component :is="titleComponent">
<template #delete-button> <template #delete-button>
<gl-button <gl-button
v-if="canDelete" v-if="canDelete"
...@@ -173,7 +180,7 @@ export default { ...@@ -173,7 +180,7 @@ export default {
{{ __('Delete') }} {{ __('Delete') }}
</gl-button> </gl-button>
</template> </template>
</package-title> </component>
<gl-tabs> <gl-tabs>
<gl-tab :title="__('Detail')"> <gl-tab :title="__('Detail')">
......
<script> <script>
import { PackageType } from '../../shared/constants'; import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
import { PackageType, TERRAFORM_PACKAGE_TYPE } from '../../shared/constants';
import ComposerInstallation from './composer_installation.vue'; import ComposerInstallation from './composer_installation.vue';
import ConanInstallation from './conan_installation.vue'; import ConanInstallation from './conan_installation.vue';
import MavenInstallation from './maven_installation.vue'; import MavenInstallation from './maven_installation.vue';
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
[PackageType.NUGET]: NugetInstallation, [PackageType.NUGET]: NugetInstallation,
[PackageType.PYPI]: PypiInstallation, [PackageType.PYPI]: PypiInstallation,
[PackageType.COMPOSER]: ComposerInstallation, [PackageType.COMPOSER]: ComposerInstallation,
[TERRAFORM_PACKAGE_TYPE]: TerraformInstallation,
}, },
props: { props: {
packageEntity: { packageEntity: {
......
...@@ -11,6 +11,9 @@ export const PackageType = { ...@@ -11,6 +11,9 @@ export const PackageType = {
GENERIC: 'generic', GENERIC: 'generic',
}; };
// we want this separated from the main dictionary to avoid it being pulled in the search of package
export const TERRAFORM_PACKAGE_TYPE = 'terraform_module';
export const TrackingActions = { export const TrackingActions = {
DELETE_PACKAGE: 'delete_package', DELETE_PACKAGE: 'delete_package',
REQUEST_DELETE_PACKAGE: 'request_delete_package', REQUEST_DELETE_PACKAGE: 'request_delete_package',
......
<script>
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
name: 'DetailsTitle',
components: {
TitleArea,
GlIcon,
GlSprintf,
MetadataItem,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
computed: {
...mapState(['packageEntity', 'packageFiles']),
...mapGetters(['packagePipeline']),
totalSize() {
return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
},
},
methods: {
dynamicSlotName(index) {
return `metadata-tag${index}`;
},
},
};
</script>
<template>
<title-area :title="packageEntity.name" data-qa-selector="package_title">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
<template #version>
{{ packageEntity.version }}
</template>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
</template>
<template #metadata-type>
<metadata-item data-testid="package-type" icon="infrastructure-registry" text="Terraform" />
</template>
<template #metadata-size>
<metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
</template>
<template v-if="packagePipeline" #metadata-pipeline>
<metadata-item
data-testid="pipeline-project"
icon="review-list"
:text="packagePipeline.project.name"
:link="packagePipeline.project.web_url"
/>
</template>
<template v-if="packagePipeline" #metadata-ref>
<metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
</template>
<template #right-actions>
<slot name="delete-button"></slot>
</template>
</title-area>
</template>
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex';
import { s__ } from '~/locale';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
export default {
name: 'ConanInstallation',
components: {
CodeInstruction,
GlLink,
GlSprintf,
},
computed: {
...mapState(['packageEntity', 'terraformHelpPath', 'projectPath']),
provisionInstructions() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `module "${this.packageEntity.name}" {
source = "${this.projectPath}/${this.packageEntity.name}"
version = "${this.packageEntity.version}"
}`;
},
registrySetup() {
return `credentials "gitlab.com" {
token = "<TOKEN>"
}`;
},
},
i18n: {
helpText: s__(
'InfrastructureRegistry|For more information on the Terraform registry, %{linkStart}see our documentation%{linkEnd}.',
),
},
};
</script>
<template>
<div>
<h3 class="gl-font-lg">{{ __('Provision instructions') }}</h3>
<code-instruction
:label="
s__(
'InfrastructureRegistry|Copy and paste into your Terraform configuration, insert the variables, and run Terraform init:',
)
"
:instruction="provisionInstructions"
:copy-text="s__('InfrastructureRegistry|Copy Terraform Command')"
multiline
/>
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
<code-instruction
:label="s__('InfrastructureRegistry|To authorize access to the Terraform registry:')"
:instruction="registrySetup"
:copy-text="s__('InfrastructureRegistry|Copy Terraform Setup Command')"
multiline
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
<gl-link :href="terraformHelpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
</template>
...@@ -22,6 +22,9 @@ export default () => { ...@@ -22,6 +22,9 @@ export default () => {
return new Vue({ return new Vue({
el, el,
store, store,
provide: {
titleComponent: 'TerraformTitle',
},
render(createElement) { render(createElement) {
return createElement(PackagesApp); return createElement(PackagesApp);
}, },
......
...@@ -10,4 +10,6 @@ ...@@ -10,4 +10,6 @@
can_delete: can?(current_user, :destroy_package, @project).to_s, can_delete: can?(current_user, :destroy_package, @project).to_s,
svg_path: image_path('illustrations/no-packages.svg'), svg_path: image_path('illustrations/no-packages.svg'),
project_name: @project.name, project_name: @project.name,
project_path: expose_url(@project.full_path),
terraform_help_path: help_page_path('user/infrastructure/index'),
project_list_url: project_infrastructure_registry_index_path(@project)} } project_list_url: project_infrastructure_registry_index_path(@project)} }
...@@ -17444,6 +17444,18 @@ msgstr "" ...@@ -17444,6 +17444,18 @@ msgstr ""
msgid "Infrastructure Registry" msgid "Infrastructure Registry"
msgstr "" msgstr ""
msgid "InfrastructureRegistry|Copy Terraform Command"
msgstr ""
msgid "InfrastructureRegistry|Copy Terraform Setup Command"
msgstr ""
msgid "InfrastructureRegistry|Copy and paste into your Terraform configuration, insert the variables, and run Terraform init:"
msgstr ""
msgid "InfrastructureRegistry|For more information on the Terraform registry, %{linkStart}see our documentation%{linkEnd}."
msgstr ""
msgid "InfrastructureRegistry|Infrastructure Registry" msgid "InfrastructureRegistry|Infrastructure Registry"
msgstr "" msgstr ""
...@@ -17456,6 +17468,9 @@ msgstr "" ...@@ -17456,6 +17468,9 @@ msgstr ""
msgid "InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab." msgid "InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab."
msgstr "" msgstr ""
msgid "InfrastructureRegistry|To authorize access to the Terraform registry:"
msgstr ""
msgid "InfrastructureRegistry|You have no Terraform modules in your project" msgid "InfrastructureRegistry|You have no Terraform modules in your project"
msgstr "" msgstr ""
...@@ -26597,6 +26612,9 @@ msgstr "" ...@@ -26597,6 +26612,9 @@ msgstr ""
msgid "Provider" msgid "Provider"
msgstr "" msgstr ""
msgid "Provision instructions"
msgstr ""
msgid "Provisioned by:" msgid "Provisioned by:"
msgstr "" msgstr ""
......
import { GlEmptyState } from '@gitlab/ui'; import { GlEmptyState } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
...@@ -109,9 +110,11 @@ describe('PackagesApp', () => { ...@@ -109,9 +110,11 @@ describe('PackagesApp', () => {
window.location = location; window.location = location;
}); });
it('renders the app and displays the package title', () => { it('renders the app and displays the package title', async () => {
createComponent(); createComponent();
await nextTick();
expect(packageTitle().exists()).toBe(true); expect(packageTitle().exists()).toBe(true);
}); });
......
...@@ -7,6 +7,7 @@ import MavenInstallation from '~/packages/details/components/maven_installation. ...@@ -7,6 +7,7 @@ import MavenInstallation from '~/packages/details/components/maven_installation.
import NpmInstallation from '~/packages/details/components/npm_installation.vue'; import NpmInstallation from '~/packages/details/components/npm_installation.vue';
import NugetInstallation from '~/packages/details/components/nuget_installation.vue'; import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
import PypiInstallation from '~/packages/details/components/pypi_installation.vue'; import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
import { import {
conanPackage, conanPackage,
...@@ -15,6 +16,7 @@ import { ...@@ -15,6 +16,7 @@ import {
nugetPackage, nugetPackage,
pypiPackage, pypiPackage,
composerPackage, composerPackage,
terraformModule,
} from '../../mock_data'; } from '../../mock_data';
describe('InstallationCommands', () => { describe('InstallationCommands', () => {
...@@ -32,6 +34,7 @@ describe('InstallationCommands', () => { ...@@ -32,6 +34,7 @@ describe('InstallationCommands', () => {
const nugetInstallation = () => wrapper.find(NugetInstallation); const nugetInstallation = () => wrapper.find(NugetInstallation);
const pypiInstallation = () => wrapper.find(PypiInstallation); const pypiInstallation = () => wrapper.find(PypiInstallation);
const composerInstallation = () => wrapper.find(ComposerInstallation); const composerInstallation = () => wrapper.find(ComposerInstallation);
const terraformInstallation = () => wrapper.findComponent(TerraformInstallation);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -46,6 +49,7 @@ describe('InstallationCommands', () => { ...@@ -46,6 +49,7 @@ describe('InstallationCommands', () => {
${nugetPackage} | ${nugetInstallation} ${nugetPackage} | ${nugetInstallation}
${pypiPackage} | ${pypiInstallation} ${pypiPackage} | ${pypiInstallation}
${composerPackage} | ${composerInstallation} ${composerPackage} | ${composerInstallation}
${terraformModule} | ${terraformInstallation}
`('renders', ({ packageEntity, selector }) => { `('renders', ({ packageEntity, selector }) => {
it(`${packageEntity.package_type} instructions exist`, () => { it(`${packageEntity.package_type} instructions exist`, () => {
createComponent({ packageEntity }); createComponent({ packageEntity });
......
...@@ -178,6 +178,20 @@ export const composerPackage = { ...@@ -178,6 +178,20 @@ export const composerPackage = {
version: '1.0.0', version: '1.0.0',
}; };
export const terraformModule = {
created_at: '2015-12-10',
id: 2,
name: 'Test/system-22',
package_type: 'terraform_module',
project_path: 'foo/bar/baz',
projectPathName: 'foo/bar/baz',
project_id: 1,
updated_at: '2015-12-10',
version: '0.1',
versions: [],
_links,
};
export const mockTags = [ export const mockTags = [
{ {
name: 'foo-1', name: 'foo-1',
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TerraformInstallation renders all the messages 1`] = `
<div>
<h3
class="gl-font-lg"
>
Provision instructions
</h3>
<code-instruction-stub
copytext="Copy Terraform Command"
instruction="module \\"Test/system-22\\" {
source = \\"foo/Test/system-22\\"
version = \\"0.1\\"
}"
label="Copy and paste into your Terraform configuration, insert the variables, and run Terraform init:"
multiline="true"
trackingaction=""
trackinglabel=""
/>
<h3
class="gl-font-lg"
>
Registry setup
</h3>
<code-instruction-stub
copytext="Copy Terraform Setup Command"
instruction="credentials \\"gitlab.com\\" {
token = \\"<TOKEN>\\"
}"
label="To authorize access to the Terraform registry:"
multiline="true"
trackingaction=""
trackinglabel=""
/>
<gl-sprintf-stub
message="For more information on the Terraform registry, %{linkStart}see our documentation%{linkEnd}."
/>
</div>
`;
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { terraformModule, mavenFiles, npmPackage } from 'jest/packages/mock_data';
import component from '~/packages_and_registries/infrastructure_registry/components/details_title.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('PackageTitle', () => {
let wrapper;
let store;
function createComponent({ packageFiles = mavenFiles, packageEntity = terraformModule } = {}) {
store = new Vuex.Store({
state: {
packageEntity,
packageFiles,
},
getters: {
packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
},
});
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
TitleArea,
},
});
return wrapper.vm.$nextTick();
}
const findTitleArea = () => wrapper.findComponent(TitleArea);
const packageSize = () => wrapper.find('[data-testid="package-size"]');
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]');
afterEach(() => {
wrapper.destroy();
});
describe('module title', () => {
it('is correctly bound', async () => {
await createComponent();
expect(findTitleArea().props('title')).toBe(terraformModule.name);
});
});
describe('calculates the package size', () => {
it('correctly calculates the size', async () => {
await createComponent();
expect(packageSize().props('text')).toBe('300 bytes');
});
});
describe('package ref', () => {
it('does not display the ref if missing', async () => {
await createComponent();
expect(packageRef().exists()).toBe(false);
});
it('correctly shows the package ref if there is one', async () => {
await createComponent({ packageEntity: npmPackage });
expect(packageRef().props()).toMatchObject({
text: npmPackage.pipeline.ref,
icon: 'branch',
});
});
});
describe('pipeline project', () => {
it('does not display the project if missing', async () => {
await createComponent();
expect(pipelineProject().exists()).toBe(false);
});
it('correctly shows the pipeline project if there is one', async () => {
await createComponent({ packageEntity: npmPackage });
expect(pipelineProject().props()).toMatchObject({
text: npmPackage.pipeline.project.name,
icon: 'review-list',
link: npmPackage.pipeline.project.web_url,
});
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { terraformModule as packageEntity } from 'jest/packages/mock_data';
import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('TerraformInstallation', () => {
let wrapper;
const store = new Vuex.Store({
state: {
packageEntity,
projectPath: 'foo',
},
});
const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
function createComponent() {
wrapper = shallowMount(TerraformInstallation, {
localVue,
store,
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('installation commands', () => {
it('renders the correct command', () => {
expect(findCodeInstructions().at(0).props('instruction')).toMatchInlineSnapshot(`
"module \\"Test/system-22\\" {
source = \\"foo/Test/system-22\\"
version = \\"0.1\\"
}"
`);
});
});
describe('setup commands', () => {
it('renders the correct command', () => {
expect(findCodeInstructions().at(1).props('instruction')).toMatchInlineSnapshot(`
"credentials \\"gitlab.com\\" {
token = \\"<TOKEN>\\"
}"
`);
});
});
});
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