gitlab-org--gitlab-foss/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue

322 lines
9.6 KiB
Vue

<script>
import {
GlAlert,
GlButton,
GlModal,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlIcon,
GlLoadingIcon,
GlSkeletonLoader,
GlResizeObserverDirective,
} from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { isEmpty } from 'lodash';
import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import {
INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
REGISTRATION_TOKEN_PLACEHOLDER,
} from './constants';
import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql';
export default {
components: {
GlAlert,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlModal,
GlIcon,
GlLoadingIcon,
GlSkeletonLoader,
ModalCopyButton,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
props: {
modalId: {
type: String,
required: false,
default: 'runner-instructions-modal',
},
registrationToken: {
type: String,
required: false,
default: null,
},
defaultPlatformName: {
type: String,
required: false,
default: null,
},
},
apollo: {
platforms: {
query: getRunnerPlatformsQuery,
skip() {
// Only load instructions once the modal is shown
return !this.shown;
},
update(data) {
return (
data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
return {
name,
humanReadableName,
architectures: architectures?.nodes || [],
};
}) ?? []
);
},
result() {
// If it is set and available, select the defaultSelectedPlatform.
// Otherwise, select the first available platform
this.selectPlatform(this.defaultPlatformName || this.platforms?.[0].name);
},
error() {
this.toggleAlert(true);
},
},
instructions: {
query: getRunnerSetupInstructionsQuery,
skip() {
return !this.shown || !this.selectedPlatform;
},
variables() {
return {
platform: this.selectedPlatform,
architecture: this.selectedArchitecture || '',
};
},
update(data) {
return data?.runnerSetup;
},
error() {
this.toggleAlert(true);
},
},
},
data() {
return {
shown: false,
platforms: [],
selectedPlatform: null,
selectedArchitecture: null,
showAlert: false,
instructions: {},
platformsButtonGroupVertical: false,
};
},
computed: {
instructionsEmpty() {
return isEmpty(this.instructions);
},
architectures() {
return this.platforms.find(({ name }) => name === this.selectedPlatform)?.architectures || [];
},
binaryUrl() {
return this.architectures.find(({ name }) => name === this.selectedArchitecture)
?.downloadLocation;
},
instructionsWithoutArchitecture() {
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.instructions;
},
runnerInstallationLink() {
return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.link;
},
registerInstructionsWithToken() {
const { registerInstructions } = this.instructions || {};
if (this.registrationToken) {
return registerInstructions?.replace(
REGISTRATION_TOKEN_PLACEHOLDER,
this.registrationToken,
);
}
return registerInstructions;
},
},
updated() {
// Refocus on dom changes, after loading data
this.refocusSelectedPlatformButton();
},
methods: {
show() {
this.$refs.modal.show();
},
onShown() {
this.shown = true;
this.refocusSelectedPlatformButton();
},
refocusSelectedPlatformButton() {
// On modal opening, the first focusable element is auto-focused by bootstrap-vue
// This can be confusing for users, because the wrong platform button can
// get focused when setting a `defaultPlatformName`.
// This method refocuses the expected button.
// See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open
this.$refs[this.selectedPlatform]?.[0].$el.focus();
},
selectPlatform(platformName) {
this.selectedPlatform = platformName;
// Update architecture when platform changes
const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture);
if (arch) {
this.selectArchitecture(arch.name);
} else {
this.selectArchitecture(this.architectures[0]?.name);
}
},
selectArchitecture(architecture) {
this.selectedArchitecture = architecture;
},
toggleAlert(state) {
this.showAlert = state;
},
onPlatformsButtonResize() {
if (bp.getBreakpointSize() === 'xs') {
this.platformsButtonGroupVertical = true;
} else {
this.platformsButtonGroupVertical = false;
}
},
},
i18n: {
environment: __('Environment'),
installARunner: s__('Runners|Install a runner'),
architecture: s__('Runners|Architecture'),
downloadInstallBinary: s__('Runners|Download and install binary'),
downloadLatestBinary: s__('Runners|Download latest binary'),
registerRunnerCommand: s__('Runners|Command to register runner'),
fetchError: s__('Runners|An error has occurred fetching instructions'),
copyInstructions: s__('Runners|Copy instructions'),
viewInstallationInstructions: s__('Runners|View installation instructions'),
},
closeButton: {
text: __('Close'),
attributes: [{ variant: 'default' }],
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="modalId"
:title="$options.i18n.installARunner"
:action-secondary="$options.closeButton"
v-bind="$attrs"
v-on="$listeners"
@shown="onShown"
>
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
{{ $options.i18n.fetchError }}
</gl-alert>
<gl-skeleton-loader v-if="!platforms.length && $apollo.loading" />
<template v-if="platforms.length">
<h5>
{{ $options.i18n.environment }}
</h5>
<div v-gl-resize-observer="onPlatformsButtonResize">
<gl-button-group
:vertical="platformsButtonGroupVertical"
:class="{ 'gl-w-full': platformsButtonGroupVertical }"
class="gl-mb-3"
data-testid="platform-buttons"
>
<gl-button
v-for="platform in platforms"
:key="platform.name"
:ref="platform.name"
:selected="selectedPlatform === platform.name"
@click="selectPlatform(platform.name)"
>
{{ platform.humanReadableName }}
</gl-button>
</gl-button-group>
</div>
</template>
<template v-if="architectures.length">
<template v-if="selectedPlatform">
<h5>
{{ $options.i18n.architecture }}
<gl-loading-icon v-if="$apollo.loading" size="sm" inline />
</h5>
<gl-dropdown class="gl-mb-3" :text="selectedArchitecture">
<gl-dropdown-item
v-for="architecture in architectures"
:key="architecture.name"
:is-check-item="true"
:is-checked="selectedArchitecture === architecture.name"
data-testid="architecture-dropdown-item"
@click="selectArchitecture(architecture.name)"
>
{{ architecture.name }}
</gl-dropdown-item>
</gl-dropdown>
<div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
v-if="binaryUrl"
class="gl-ml-auto"
:href="binaryUrl"
download
icon="download"
data-testid="binary-download-button"
>
{{ $options.i18n.downloadLatestBinary }}
</gl-button>
</div>
</template>
<template v-if="!instructionsEmpty">
<div class="gl-display-flex">
<pre
class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
data-testid="binary-instructions"
>{{ instructions.installInstructions }}</pre
>
<modal-copy-button
:title="$options.i18n.copyInstructions"
:text="instructions.installInstructions"
:modal-id="$options.modalId"
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
category="tertiary"
/>
</div>
<h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5>
<div class="gl-display-flex">
<pre
class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
data-testid="register-command"
>{{ registerInstructionsWithToken }}</pre
>
<modal-copy-button
:title="$options.i18n.copyInstructions"
:text="registerInstructionsWithToken"
:modal-id="$options.modalId"
css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
category="tertiary"
/>
</div>
</template>
</template>
<template v-else>
<div>
<p>{{ instructionsWithoutArchitecture }}</p>
<gl-button :href="runnerInstallationLink">
<gl-icon name="external-link" />
{{ $options.i18n.viewInstallationInstructions }}
</gl-button>
</div>
</template>
</gl-modal>
</template>