2022-08-08 15:10:32 +00:00
|
|
|
<script>
|
2022-08-22 18:10:26 +00:00
|
|
|
import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
|
2022-08-08 15:10:32 +00:00
|
|
|
import * as Sentry from '@sentry/browser';
|
|
|
|
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
2022-08-18 09:11:27 +00:00
|
|
|
import { sprintf, __ } from '~/locale';
|
2022-08-08 15:10:32 +00:00
|
|
|
import Poll from '~/lib/utils/poll';
|
2022-08-15 21:09:52 +00:00
|
|
|
import StatusIcon from '../extensions/status_icon.vue';
|
2022-08-23 18:11:55 +00:00
|
|
|
import ActionButtons from '../action_buttons.vue';
|
2022-08-18 12:13:06 +00:00
|
|
|
import { EXTENSION_ICONS } from '../../constants';
|
2022-08-08 15:10:32 +00:00
|
|
|
|
|
|
|
const FETCH_TYPE_COLLAPSED = 'collapsed';
|
2022-08-22 18:10:26 +00:00
|
|
|
const FETCH_TYPE_EXPANDED = 'expanded';
|
2022-08-08 15:10:32 +00:00
|
|
|
|
|
|
|
export default {
|
2022-08-15 21:09:52 +00:00
|
|
|
components: {
|
2022-08-23 18:11:55 +00:00
|
|
|
ActionButtons,
|
2022-08-15 21:09:52 +00:00
|
|
|
StatusIcon,
|
2022-08-18 09:11:27 +00:00
|
|
|
GlButton,
|
2022-08-22 18:10:26 +00:00
|
|
|
GlLoadingIcon,
|
2022-08-18 09:11:27 +00:00
|
|
|
},
|
|
|
|
directives: {
|
|
|
|
GlTooltip: GlTooltipDirective,
|
2022-08-15 21:09:52 +00:00
|
|
|
},
|
2022-08-08 15:10:32 +00:00
|
|
|
props: {
|
|
|
|
/**
|
|
|
|
* @param {value.collapsed} Object
|
2022-08-22 18:10:26 +00:00
|
|
|
* @param {value.expanded} Object
|
2022-08-08 15:10:32 +00:00
|
|
|
*/
|
|
|
|
value: {
|
|
|
|
type: Object,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
loadingText: {
|
|
|
|
type: String,
|
2022-08-15 21:09:52 +00:00
|
|
|
required: false,
|
|
|
|
default: __('Loading'),
|
2022-08-08 15:10:32 +00:00
|
|
|
},
|
|
|
|
errorText: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
|
|
|
default: __('Failed to load'),
|
|
|
|
},
|
|
|
|
fetchCollapsedData: {
|
|
|
|
type: Function,
|
|
|
|
required: true,
|
|
|
|
},
|
2022-08-22 18:10:26 +00:00
|
|
|
fetchExpandedData: {
|
2022-08-08 15:10:32 +00:00
|
|
|
type: Function,
|
|
|
|
required: false,
|
|
|
|
default: undefined,
|
|
|
|
},
|
|
|
|
// If the summary slot is not used, this value will be used as a fallback.
|
|
|
|
summary: {
|
|
|
|
type: String,
|
|
|
|
required: false,
|
|
|
|
default: undefined,
|
|
|
|
},
|
|
|
|
// If the content slot is not used, this value will be used as a fallback.
|
|
|
|
content: {
|
|
|
|
type: Object,
|
|
|
|
required: false,
|
|
|
|
default: undefined,
|
|
|
|
},
|
|
|
|
multiPolling: {
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
default: false,
|
|
|
|
},
|
2022-08-15 21:09:52 +00:00
|
|
|
statusIconName: {
|
|
|
|
type: String,
|
|
|
|
default: 'neutral',
|
|
|
|
required: false,
|
2022-08-18 12:13:06 +00:00
|
|
|
validator: (value) => Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
|
2022-08-15 21:09:52 +00:00
|
|
|
},
|
2022-08-18 09:11:27 +00:00
|
|
|
isCollapsible: {
|
|
|
|
type: Boolean,
|
|
|
|
required: true,
|
|
|
|
},
|
2022-08-23 18:11:55 +00:00
|
|
|
actionButtons: {
|
|
|
|
type: Array,
|
|
|
|
required: false,
|
|
|
|
default: () => [],
|
|
|
|
},
|
2022-08-15 21:09:52 +00:00
|
|
|
widgetName: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
2022-08-08 15:10:32 +00:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
2022-08-22 18:10:26 +00:00
|
|
|
isExpandedForTheFirstTime: true,
|
2022-08-18 09:11:27 +00:00
|
|
|
isCollapsed: true,
|
2022-08-15 21:09:52 +00:00
|
|
|
isLoading: false,
|
2022-08-22 18:10:26 +00:00
|
|
|
isLoadingExpandedContent: false,
|
2022-08-08 15:10:32 +00:00
|
|
|
error: null,
|
|
|
|
};
|
|
|
|
},
|
2022-08-18 12:13:06 +00:00
|
|
|
computed: {
|
2022-08-19 12:11:34 +00:00
|
|
|
collapseButtonLabel() {
|
|
|
|
return sprintf(this.isCollapsed ? __('Show details') : __('Hide details'));
|
|
|
|
},
|
2022-08-18 12:13:06 +00:00
|
|
|
statusIcon() {
|
|
|
|
return this.error ? EXTENSION_ICONS.failed : this.statusIconName;
|
|
|
|
},
|
|
|
|
},
|
2022-08-15 21:09:52 +00:00
|
|
|
watch: {
|
|
|
|
isLoading(newValue) {
|
|
|
|
this.$emit('is-loading', newValue);
|
|
|
|
},
|
|
|
|
},
|
2022-08-08 15:10:32 +00:00
|
|
|
async mounted() {
|
2022-08-15 21:09:52 +00:00
|
|
|
this.isLoading = true;
|
2022-08-08 15:10:32 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
|
|
|
|
} catch {
|
|
|
|
this.error = this.errorText;
|
|
|
|
}
|
|
|
|
|
2022-08-15 21:09:52 +00:00
|
|
|
this.isLoading = false;
|
2022-08-08 15:10:32 +00:00
|
|
|
},
|
|
|
|
methods: {
|
2022-08-18 09:11:27 +00:00
|
|
|
toggleCollapsed() {
|
|
|
|
this.isCollapsed = !this.isCollapsed;
|
2022-08-22 18:10:26 +00:00
|
|
|
|
|
|
|
if (this.isExpandedForTheFirstTime && typeof this.fetchExpandedData === 'function') {
|
|
|
|
this.isExpandedForTheFirstTime = false;
|
|
|
|
this.fetchExpandedContent();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
async fetchExpandedContent() {
|
|
|
|
this.isLoadingExpandedContent = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
|
|
|
|
} catch {
|
|
|
|
this.error = this.errorText;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isLoadingExpandedContent = false;
|
2022-08-18 09:11:27 +00:00
|
|
|
},
|
2022-08-08 15:10:32 +00:00
|
|
|
fetch(handler, dataType) {
|
|
|
|
const requests = this.multiPolling ? handler() : [handler];
|
|
|
|
|
|
|
|
const promises = requests.map((request) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const poll = new Poll({
|
|
|
|
resource: {
|
|
|
|
fetchData: () => request(),
|
|
|
|
},
|
|
|
|
method: 'fetchData',
|
|
|
|
successCallback: (response) => {
|
|
|
|
const headers = normalizeHeaders(response.headers);
|
|
|
|
|
|
|
|
if (headers['POLL-INTERVAL']) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-08-15 21:09:52 +00:00
|
|
|
resolve(response.data);
|
2022-08-08 15:10:32 +00:00
|
|
|
},
|
|
|
|
errorCallback: (e) => {
|
|
|
|
Sentry.captureException(e);
|
|
|
|
reject(e);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
poll.makeRequest();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-08-15 21:09:52 +00:00
|
|
|
return Promise.all(promises).then((data) => {
|
|
|
|
this.$emit('input', { ...this.value, [dataType]: this.multiPolling ? data : data[0] });
|
|
|
|
});
|
2022-08-08 15:10:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<section class="media-section" data-testid="widget-extension">
|
|
|
|
<div class="media gl-p-5">
|
2022-08-18 12:13:06 +00:00
|
|
|
<status-icon :level="1" :name="widgetName" :is-loading="isLoading" :icon-name="statusIcon" />
|
2022-08-08 15:10:32 +00:00
|
|
|
<div
|
|
|
|
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
|
|
|
|
data-testid="widget-extension-top-level"
|
|
|
|
>
|
|
|
|
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
|
2022-08-18 12:13:06 +00:00
|
|
|
<slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot>
|
|
|
|
<span v-else>{{ error }}</span>
|
2022-08-08 15:10:32 +00:00
|
|
|
</div>
|
2022-08-23 18:11:55 +00:00
|
|
|
<action-buttons
|
|
|
|
v-if="actionButtons.length > 0"
|
|
|
|
:widget="widgetName"
|
|
|
|
:tertiary-buttons="actionButtons"
|
|
|
|
/>
|
2022-08-18 09:11:27 +00:00
|
|
|
<div
|
|
|
|
v-if="isCollapsible"
|
|
|
|
class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"
|
|
|
|
>
|
|
|
|
<gl-button
|
|
|
|
v-gl-tooltip
|
|
|
|
:title="collapseButtonLabel"
|
|
|
|
:aria-expanded="`${!isCollapsed}`"
|
|
|
|
:aria-label="collapseButtonLabel"
|
|
|
|
:icon="isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
|
|
|
|
category="tertiary"
|
|
|
|
data-testid="toggle-button"
|
|
|
|
size="small"
|
|
|
|
@click="toggleCollapsed"
|
|
|
|
/>
|
|
|
|
</div>
|
2022-08-08 15:10:32 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div
|
2022-08-18 09:11:27 +00:00
|
|
|
v-if="!isCollapsed"
|
2022-08-08 15:10:32 +00:00
|
|
|
class="mr-widget-grouped-section gl-relative"
|
|
|
|
data-testid="widget-extension-collapsed-section"
|
|
|
|
>
|
2022-08-22 18:10:26 +00:00
|
|
|
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
|
|
|
|
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
|
|
|
|
</div>
|
|
|
|
<slot v-else name="content">{{ content }}</slot>
|
2022-08-08 15:10:32 +00:00
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</template>
|