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-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-10-03 15:09:42 +00:00
|
|
|
import ContentRow from './widget_content_row.vue';
|
|
|
|
import DynamicContent from './dynamic_content.vue';
|
|
|
|
import StatusIcon from './status_icon.vue';
|
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-10-03 15:09:42 +00:00
|
|
|
ContentRow,
|
|
|
|
DynamicContent,
|
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: {
|
2022-10-03 15:09:42 +00:00
|
|
|
type: Array,
|
2022-08-08 15:10:32 +00:00
|
|
|
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-09-06 09:12:58 +00:00
|
|
|
summaryError: null,
|
|
|
|
contentError: null,
|
2022-08-08 15:10:32 +00:00
|
|
|
};
|
|
|
|
},
|
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-09-06 09:12:58 +00:00
|
|
|
summaryStatusIcon() {
|
|
|
|
return this.summaryError ? this.$options.failedStatusIcon : this.statusIconName;
|
2022-08-18 12:13:06 +00:00
|
|
|
},
|
|
|
|
},
|
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 {
|
2022-09-06 09:12:58 +00:00
|
|
|
this.summaryError = this.errorText;
|
2022-08-08 15:10:32 +00:00
|
|
|
}
|
|
|
|
|
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;
|
2022-09-06 09:12:58 +00:00
|
|
|
this.contentError = null;
|
2022-08-22 18:10:26 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
|
|
|
|
} catch {
|
2022-09-06 09:12:58 +00:00
|
|
|
this.contentError = this.errorText;
|
2022-08-24 15:12:19 +00:00
|
|
|
|
|
|
|
// Reset these values so that we allow refetching
|
|
|
|
this.isExpandedForTheFirstTime = true;
|
|
|
|
this.isCollapsed = true;
|
2022-08-22 18:10:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
},
|
|
|
|
},
|
2022-09-06 09:12:58 +00:00
|
|
|
failedStatusIcon: EXTENSION_ICONS.failed,
|
2022-08-08 15:10:32 +00:00
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<section class="media-section" data-testid="widget-extension">
|
2022-09-19 18:10:34 +00:00
|
|
|
<div class="gl-p-5 gl-align-items-center gl-display-flex">
|
2022-09-06 09:12:58 +00:00
|
|
|
<status-icon
|
|
|
|
:level="1"
|
|
|
|
:name="widgetName"
|
|
|
|
:is-loading="isLoading"
|
|
|
|
:icon-name="summaryStatusIcon"
|
|
|
|
/>
|
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-09-06 09:12:58 +00:00
|
|
|
<span v-if="summaryError">{{ summaryError }}</span>
|
|
|
|
<slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot>
|
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-09-06 09:12:58 +00:00
|
|
|
v-if="!isCollapsed || contentError"
|
2022-10-03 15:09:42 +00:00
|
|
|
class="gl-relative gl-bg-gray-10"
|
2022-08-08 15:10:32 +00:00
|
|
|
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">
|
2022-10-03 15:09:42 +00:00
|
|
|
<gl-loading-icon size="sm" inline /> {{ loadingText }}
|
|
|
|
</div>
|
|
|
|
<div v-else class="gl-px-5 gl-display-flex">
|
|
|
|
<content-row
|
|
|
|
v-if="contentError"
|
|
|
|
:level="2"
|
|
|
|
:status-icon-name="$options.failedStatusIcon"
|
|
|
|
:widget-name="widgetName"
|
|
|
|
>
|
|
|
|
<template #body>
|
|
|
|
{{ contentError }}
|
|
|
|
</template>
|
|
|
|
</content-row>
|
2022-10-12 12:09:35 +00:00
|
|
|
<div v-else class="gl-w-full">
|
|
|
|
<slot name="content">
|
2022-10-03 15:09:42 +00:00
|
|
|
<dynamic-content
|
|
|
|
v-for="(data, index) in content"
|
|
|
|
:key="data.id || index"
|
|
|
|
:data="data"
|
|
|
|
:widget-name="widgetName"
|
|
|
|
/>
|
2022-10-12 12:09:35 +00:00
|
|
|
</slot>
|
|
|
|
</div>
|
2022-08-22 18:10:26 +00:00
|
|
|
</div>
|
2022-08-08 15:10:32 +00:00
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</template>
|