gitlab-org--gitlab-foss/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue

355 lines
11 KiB
Vue

<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue';
import {
COMMIT_SHA_POLL_INTERVAL,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
ConfirmUnsavedChangesDialog,
GlLoadingIcon,
PipelineEditorEmptyState,
PipelineEditorHome,
PipelineEditorMessages,
},
inject: {
ciConfigPath: {
default: '',
},
projectFullPath: {
default: '',
},
},
data() {
return {
starterTemplateName: STARTER_TEMPLATE_NAME,
ciConfigData: {},
failureType: null,
failureReasons: [],
initialCiFileContent: '',
isFetchingCommitSha: false,
isNewCiConfigFile: false,
lastCommittedContent: '',
currentCiFileContent: '',
successType: null,
showStartScreen: false,
showSuccess: false,
showFailure: false,
starterTemplate: '',
};
},
apollo: {
initialCiFileContent: {
fetchPolicy: fetchPolicies.NETWORK_ONLY,
query: getBlobContent,
// If it's a brand new file, we don't want to fetch the content.
// Then when the user commits the first time, the query would run
// to get the initial file content, but we already have it in `lastCommitedContent`
// so we skip the loading altogether.
skip({ isNewCiConfigFile, lastCommittedContent }) {
return isNewCiConfigFile || lastCommittedContent;
},
variables() {
return {
projectPath: this.projectFullPath,
path: this.ciConfigPath,
ref: this.currentBranch,
};
},
update(data) {
return data?.project?.repository?.blobs?.nodes[0]?.rawBlob;
},
result({ data }) {
const nodes = data?.project?.repository?.blobs?.nodes;
if (!nodes) {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
} else {
const rawBlob = nodes[0]?.rawBlob;
const fileContent = rawBlob ?? '';
this.lastCommittedContent = fileContent;
this.currentCiFileContent = fileContent;
// If rawBlob is defined and returns a string, it means that there is
// a CI config file with empty content. If `rawBlob` is not defined
// at all, it means there was no file found.
const hasCIFile = rawBlob === '' || fileContent.length > 0;
if (!fileContent.length) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
if (!hasCIFile) {
this.showStartScreen = true;
} else if (fileContent.length) {
// If the file content is > 0, then we make sure to reset the
// start screen flag during a refetch
// e.g. when switching branches
this.showStartScreen = false;
}
}
},
error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
},
watchLoading(isLoading) {
if (isLoading) {
this.setAppStatus(EDITOR_APP_STATUS_LOADING);
}
},
},
ciConfigData: {
query: getCiConfigData,
skip({ currentCiFileContent }) {
return !currentCiFileContent;
},
variables() {
return {
projectPath: this.projectFullPath,
sha: this.commitSha,
content: this.currentCiFileContent,
};
},
update(data) {
const { ciConfig } = data || {};
const stageNodes = ciConfig?.stages?.nodes || [];
const stages = unwrapStagesWithNeeds(JSON.parse(JSON.stringify(stageNodes)));
return { ...ciConfig, stages };
},
result({ data }) {
this.setAppStatus(data?.ciConfig?.status || EDITOR_APP_STATUS_ERROR);
},
error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
},
watchLoading(isLoading) {
if (isLoading) {
this.setAppStatus(EDITOR_APP_STATUS_LOADING);
}
},
},
appStatus: {
query: getAppStatus,
},
commitSha: {
query: getLatestCommitShaQuery,
variables() {
return {
projectPath: this.projectFullPath,
ref: this.currentBranch,
};
},
update(data) {
const pipelineNodes = data.project?.pipelines?.nodes ?? [];
// it's possible to query for the commit sha too early after an update
// (e.g. after committing a new branch, we might query for the commit sha
// but the pipeline nodes are still empty).
// in this case, we start polling until we get a commit sha.
if (pipelineNodes.length === 0) {
if (![EDITOR_APP_STATUS_LOADING, EDITOR_APP_STATUS_EMPTY].includes(this.appStatus)) {
this.$apollo.queries.commitSha.startPolling(COMMIT_SHA_POLL_INTERVAL);
return this.commitSha;
}
return '';
}
const latestCommitSha = pipelineNodes[0].sha;
if (this.isFetchingCommitSha && latestCommitSha === this.commitSha) {
this.$apollo.queries.commitSha.startPolling(COMMIT_SHA_POLL_INTERVAL);
return this.commitSha;
}
this.isFetchingCommitSha = false;
this.$apollo.queries.commitSha.stopPolling();
return latestCommitSha;
},
},
currentBranch: {
query: getCurrentBranch,
},
isNewCiConfigFile: {
query: getIsNewCiConfigFile,
},
starterTemplate: {
query: getTemplate,
variables() {
return {
projectPath: this.projectFullPath,
templateName: this.starterTemplateName,
};
},
skip({ isNewCiConfigFile }) {
return !isNewCiConfigFile;
},
update(data) {
return data.project?.ciTemplate?.content || '';
},
result({ data }) {
this.updateCiConfig(data.project?.ciTemplate?.content || '');
},
error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
},
},
},
computed: {
hasUnsavedChanges() {
return this.lastCommittedContent !== this.currentCiFileContent;
},
isBlobContentLoading() {
return this.$apollo.queries.initialCiFileContent.loading;
},
isCiConfigDataLoading() {
return this.$apollo.queries.ciConfigData.loading;
},
isEmpty() {
return this.currentCiFileContent === '';
},
},
i18n: {
tabEdit: s__('Pipelines|Edit'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
watch: {
isEmpty(flag) {
if (flag) {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
},
},
mounted() {
this.loadTemplateFromURL();
},
methods: {
hideFailure() {
this.showFailure = false;
},
hideSuccess() {
this.showSuccess = false;
},
async refetchContent() {
this.$apollo.queries.initialCiFileContent.skip = false;
await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showFailure = true;
this.failureType = type;
this.failureReasons = reasons;
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showSuccess = true;
this.successType = type;
},
resetContent() {
this.currentCiFileContent = this.lastCommittedContent;
},
setAppStatus(appStatus) {
this.$apollo.getClient().writeQuery({ query: getAppStatus, data: { appStatus } });
},
setNewEmptyCiConfigFile() {
this.$apollo
.getClient()
.writeQuery({ query: getIsNewCiConfigFile, data: { isNewCiConfigFile: true } });
this.showStartScreen = false;
},
showErrorAlert({ type, reasons = [] }) {
this.reportFailure(type, reasons);
},
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
updateCommitSha() {
this.isFetchingCommitSha = true;
this.$apollo.queries.commitSha.refetch();
},
updateOnCommit({ type }) {
this.reportSuccess(type);
if (this.isNewCiConfigFile) {
this.$apollo
.getClient()
.writeQuery({ query: getIsNewCiConfigFile, data: { isNewCiConfigFile: false } });
}
// Keep track of the latest committed content to know
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
loadTemplateFromURL() {
const templateName = queryToObject(window.location.search)?.template;
if (templateName) {
this.starterTemplateName = templateName;
this.setNewEmptyCiConfigFile();
}
},
},
};
</script>
<template>
<div class="gl-mt-4 gl-relative">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
v-else-if="showStartScreen"
@createEmptyConfigFile="setNewEmptyCiConfigFile"
@refetchContent="refetchContent"
/>
<div v-else>
<pipeline-editor-messages
:failure-type="failureType"
:failure-reasons="failureReasons"
:show-failure="showFailure"
:show-success="showSuccess"
:success-type="successType"
@hide-success="hideSuccess"
@hide-failure="hideFailure"
/>
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
:is-new-ci-config-file="isNewCiConfigFile"
:commit-sha="commitSha"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
@updateCommitSha="updateCommitSha"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
</div>
</template>