debian-mirror-gitlab/app/assets/javascripts/jobs/components/job_log_controllers.vue
2022-08-27 11:52:29 +05:30

265 lines
7.5 KiB
Vue

<script>
import { GlTooltipDirective, GlLink, GlButton, GlSearchBoxByClick } from '@gitlab/ui';
import { scrollToElement, backOff } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, s__, sprintf } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
i18n: {
scrollToBottomButtonLabel: s__('Job|Scroll to bottom'),
scrollToTopButtonLabel: s__('Job|Scroll to top'),
scrollToNextFailureButtonLabel: s__('Job|Scroll to next failure'),
showRawButtonLabel: s__('Job|Show complete raw'),
searchPlaceholder: s__('Job|Search job log'),
noResults: s__('Job|No search results found'),
searchPopoverTitle: s__('Job|Job log search'),
searchPopoverDescription: s__(
'Job|Search for substrings in your job log output. Currently search is only supported for the visible job log output, not for any log output that is truncated due to size.',
),
logLineNumberNotFound: s__('Job|We could not find this element'),
},
components: {
GlLink,
GlButton,
GlSearchBoxByClick,
HelpPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
size: {
type: Number,
required: true,
},
rawPath: {
type: String,
required: false,
default: null,
},
isScrollTopDisabled: {
type: Boolean,
required: true,
},
isScrollBottomDisabled: {
type: Boolean,
required: true,
},
isScrollingDown: {
type: Boolean,
required: true,
},
isJobLogSizeVisible: {
type: Boolean,
required: true,
},
isComplete: {
type: Boolean,
required: true,
},
jobLog: {
type: Array,
required: true,
},
},
data() {
return {
searchTerm: '',
searchResults: [],
failureCount: null,
failureIndex: 0,
};
},
computed: {
jobLogSize() {
return sprintf(__('Showing last %{size} of log -'), {
size: numberToHumanSize(this.size),
});
},
showJumpToFailures() {
return this.glFeatures.jobLogJumpToFailures;
},
hasFailures() {
return this.failureCount > 0;
},
shouldDisableJumpToFailures() {
return !this.hasFailures;
},
},
mounted() {
this.checkFailureCount();
},
methods: {
checkFailureCount() {
if (this.glFeatures.jobLogJumpToFailures) {
backOff((next, stop) => {
this.failureCount = document.querySelectorAll('.term-fg-l-red').length;
if (this.hasFailures || (this.isComplete && !this.hasFailures)) {
stop();
} else {
next();
}
});
}
},
handleScrollToNextFailure() {
const failures = document.querySelectorAll('.term-fg-l-red');
const nextFailure = failures[this.failureIndex];
if (nextFailure) {
nextFailure.scrollIntoView({ block: 'center' });
this.failureIndex = (this.failureIndex + 1) % failures.length;
}
},
handleScrollToTop() {
this.$emit('scrollJobLogTop');
this.failureIndex = 0;
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
this.failureIndex = 0;
},
searchJobLog() {
this.searchResults = [];
if (!this.searchTerm) return;
const compactedLog = [];
this.jobLog.forEach((obj) => {
if (obj.lines && obj.lines.length > 0) {
compactedLog.push(...obj.lines);
}
if (!obj.lines && obj.content.length > 0) {
compactedLog.push(obj);
}
});
compactedLog.forEach((line) => {
const lineText = line.content[0].text;
if (lineText.toLocaleLowerCase().includes(this.searchTerm.toLocaleLowerCase())) {
this.searchResults.push(line);
}
});
if (this.searchResults.length > 0) {
this.$emit('searchResults', this.searchResults);
// BE returns zero based index, we need to add one to match the line numbers in the DOM
const firstSearchResult = `#L${this.searchResults[0].lineNumber + 1}`;
const logLine = document.querySelector(`.js-line ${firstSearchResult}`);
if (logLine) {
setTimeout(() => scrollToElement(logLine));
const message = sprintf(s__('Job|%{searchLength} results found for %{searchTerm}'), {
searchLength: this.searchResults.length,
searchTerm: this.searchTerm,
});
this.$toast.show(message);
} else {
this.$toast.show(this.$options.i18n.logLineNumberNotFound);
}
} else {
this.$toast.show(this.$options.i18n.noResults);
}
},
},
};
</script>
<template>
<div class="top-bar gl-display-flex gl-justify-content-space-between">
<!-- truncate information -->
<div
class="truncated-info gl-display-none gl-sm-display-flex gl-flex-wrap gl-align-items-center"
data-testid="log-truncated-info"
>
<template v-if="isJobLogSizeVisible">
{{ jobLogSize }}
<gl-link
v-if="rawPath"
:href="rawPath"
class="text-plain text-underline gl-ml-2"
data-testid="raw-link"
>{{ s__('Job|Complete Raw') }}</gl-link
>
</template>
</div>
<!-- eo truncate information -->
<div class="controllers">
<gl-search-box-by-click
v-model="searchTerm"
class="gl-mr-3"
:placeholder="$options.i18n.searchPlaceholder"
data-testid="job-log-search-box"
@clear="$emit('searchResults', [])"
@submit="searchJobLog"
/>
<help-popover class="gl-mr-3">
<template #title>{{ $options.i18n.searchPopoverTitle }}</template>
<p class="gl-mb-0">
{{ $options.i18n.searchPopoverDescription }}
</p>
</help-popover>
<!-- links -->
<gl-button
v-if="rawPath"
v-gl-tooltip.body
:title="$options.i18n.showRawButtonLabel"
:aria-label="$options.i18n.showRawButtonLabel"
:href="rawPath"
data-testid="job-raw-link-controller"
icon="doc-text"
/>
<!-- eo links -->
<!-- scroll buttons -->
<gl-button
v-if="showJumpToFailures"
v-gl-tooltip
:title="$options.i18n.scrollToNextFailureButtonLabel"
:aria-label="$options.i18n.scrollToNextFailureButtonLabel"
:disabled="shouldDisableJumpToFailures"
class="btn-scroll gl-ml-3"
data-testid="job-controller-scroll-to-failure"
icon="soft-wrap"
@click="handleScrollToNextFailure"
/>
<div v-gl-tooltip :title="$options.i18n.scrollToTopButtonLabel" class="gl-ml-3">
<gl-button
:disabled="isScrollTopDisabled"
class="btn-scroll"
data-testid="job-controller-scroll-top"
icon="scroll_up"
:aria-label="$options.i18n.scrollToTopButtonLabel"
@click="handleScrollToTop"
/>
</div>
<div v-gl-tooltip :title="$options.i18n.scrollToBottomButtonLabel" class="gl-ml-3">
<gl-button
:disabled="isScrollBottomDisabled"
class="js-scroll-bottom btn-scroll"
data-testid="job-controller-scroll-bottom"
icon="scroll_down"
:class="{ animate: isScrollingDown }"
:aria-label="$options.i18n.scrollToBottomButtonLabel"
@click="handleScrollToBottom"
/>
</div>
<!-- eo scroll buttons -->
</div>
</div>
</template>