packages/issue-dashboard-widgets/widgets/distribution-reports/app/widget.js (404 lines of code) (raw):
import React from 'react';
import PropTypes from 'prop-types';
import {i18n} from 'hub-dashboard-addons/dist/localization';
import ConfigurableWidget from '@jetbrains/hub-widget-ui/dist/configurable-widget';
import withWidgetLoaderHOC from '@jetbrains/hub-widget-ui/dist/widget-loader';
import 'd3/d3';
import 'nvd3/nv.d3';
import 'nvd3/nv.d3.css';
import ReportModel from '../../../../lib/reporting-components/report-model/report-model';
import BackendTypes from '../../../../lib/reporting-components/backend-types/backend-types';
import {
loadIssuesDistributionReports,
getYouTrackService,
loadIssueDistributionReportWithData,
recalculateReport,
saveReportSettings
} from '../../../../lib/reporting-components/resources/resources';
import fetcher from '../../../../lib/reporting-components/fetcher/fetcher';
import Configuration from './configuration';
import {getReportTypePathPrefix} from './distribution-report-types';
import DistributionReportAxises from './distribution-report-axises';
import Content from './content';
import './style/distribution-reports-widget.scss';
class DistributionReportsWidget extends React.Component {
// eslint-disable-next-line no-magic-numbers
static DEFAULT_REFRESH_PERIOD = 900;
// eslint-disable-next-line no-magic-numbers
static PROGRESS_BAR_REFRESH_PERIOD = 0.5;
// eslint-disable-next-line complexity
static applyReportSettingsFromWidgetConfig = (report, config) => {
if (!config || config.reportId !== (report || {}).id) {
return report;
}
if (!DistributionReportAxises.SortOrder.isEditable(report)) {
if (config.mainAxisSortOrder) {
DistributionReportAxises.SortOrder.setMainAxisSortOrder(
report, config.mainAxisSortOrder
);
}
if (config.secondaryAxisSortOrder) {
DistributionReportAxises.SortOrder.setSecondaryAxisSortOrder(
report, config.secondaryAxisSortOrder
);
}
if (config.presentation) {
report.presentation = config.presentation;
}
}
return report;
};
static getDefaultWidgetTitle = () =>
i18n('Issue Distribution Report');
static responseReportStatusToError = errStatus => {
if (errStatus === ReportModel.ResponseStatus.NO_ACCESS) {
return ReportModel.ErrorTypes.NO_PERMISSIONS_FOR_REPORT;
}
if (errStatus === ReportModel.ResponseStatus.NOT_FOUND) {
return ReportModel.ErrorTypes.CANNOT_LOAD_REPORT;
}
return ReportModel.ErrorTypes.UNKNOWN_ERROR;
}
static getPresentationModeWidgetTitle = (report, youTrack) => {
if (report && report.name) {
const homeUrl = (youTrack || {}).homeUrl;
const pathReportType = getReportTypePathPrefix(report);
return {
text: report.name,
href: homeUrl && `${homeUrl}reports/${pathReportType}/${report.id}`
};
}
return DistributionReportsWidget.getDefaultWidgetTitle();
};
static getConfigAsObject = (configWrapper, fieldsToOverwrite) => {
return {
reportId: getFieldValue('reportId'),
mainAxisSortOrder: getFieldValue('mainAxisSortOrder'),
secondaryAxisSortOrder: getFieldValue('secondaryAxisSortOrder'),
presentation: getFieldValue('presentation'),
youTrack: getFieldValue('youTrack'),
refreshPeriod: getFieldValue('refreshPeriod')
};
function getFieldValue(name) {
return (fieldsToOverwrite && fieldsToOverwrite[name]) ||
configWrapper.getFieldValue(name);
}
};
static propTypes = {
dashboardApi: PropTypes.object.isRequired,
registerWidgetApi: PropTypes.func.isRequired,
editable: PropTypes.bool,
configWrapper: PropTypes.object.isRequired
};
constructor(props) {
super(props);
const {registerWidgetApi} = props;
this.state = {
isConfiguring: false,
isLoading: true,
error: ReportModel.ErrorTypes.OK,
refreshPeriod: DistributionReportsWidget.DEFAULT_REFRESH_PERIOD
};
registerWidgetApi({
onConfigure: () => {
this.setState({
isConfiguring: true,
isLoading: false,
error: ReportModel.ErrorTypes.OK
});
},
onRefresh: async () => {
if (this.state.error === ReportModel.ErrorTypes.OK) {
await this.recalculateReport();
} else {
await this.onWidgetRefresh();
}
}
});
}
componentDidMount() {
this.initialize(this.props.dashboardApi);
}
// eslint-disable-next-line complexity
initialize = async dashboardApi => {
this.setLoadingEnabled(true);
await this.props.configWrapper.init();
const youTrack = this.props.configWrapper.getFieldValue('youTrack');
const ytTrackService = await getYouTrackService(
dashboardApi, youTrack && youTrack.id
);
const hasYouTrack = ytTrackService && ytTrackService.id;
if (hasYouTrack) {
this.setYouTrack(ytTrackService);
} else {
this.setError(ReportModel.ErrorTypes.NO_YOUTRACK);
}
const isNewWidget = this.props.configWrapper.isNewConfig();
if (isNewWidget || !hasYouTrack) {
if (isNewWidget) {
this.openWidgetsSettings();
}
this.setState({
isNewWidget,
refreshPeriod: DistributionReportsWidget.DEFAULT_REFRESH_PERIOD
});
return;
}
const configReportId = this.props.configWrapper.getFieldValue('reportId');
const report = (configReportId && {id: configReportId}) ||
(await loadIssuesDistributionReports(fetcher().fetchYouTrack))[0];
if (report) {
const reportWithData = await this.loadReportWithAppliedConfigSettings(
report.id,
ytTrackService
);
const refreshPeriod =
this.props.configWrapper.getFieldValue('refreshPeriod') ||
DistributionReportsWidget.DEFAULT_REFRESH_PERIOD;
this.setState(
{report: reportWithData, refreshPeriod, isLoading: false},
() => this.recalculateIfRequired()
);
} else {
this.setLoadingEnabled(false);
this.setError(ReportModel.ErrorTypes.NO_REPORT);
return;
}
};
setYouTrack(youTrackService) {
const youTrackId = (youTrackService || {}).id;
fetcher().setYouTrack(youTrackId);
BackendTypes.setYtVersion(youTrackService.version);
this.setState({
youTrack: {
id: youTrackId,
homeUrl: youTrackService.homeUrl
}
});
}
setError(error) {
this.setState({
isLoading: false, error
});
}
setLoadingEnabled(isLoading) {
this.setState({isLoading});
}
async recalculateReport() {
const {
report,
isLoading,
refreshPeriod,
isConfiguring
} = this.state;
if (isLoading || isConfiguring || !report || !report.status ||
ReportModel.isReportCalculation(report)) {
return;
}
try {
report.status = await recalculateReport(fetcher().fetchYouTrack, report);
this.setState({report, refreshPeriod});
} catch (e) {
await this.onWidgetRefresh();
}
}
async recalculateIfRequired() {
const {report} = this.state;
if (ReportModel.isCalculationRequired(report)) {
await this.recalculateReport();
}
}
onWidgetRefresh = async () => {
const {
isConfiguring,
isCalculationCompleted,
report
} = this.state;
if (!isConfiguring && report) {
const reportWithData =
await this.loadReportWithAppliedConfigSettings(report.id);
if (reportWithData) {
this.setState({
report: reportWithData,
error: ReportModel.ErrorTypes.OK,
isLoading: false,
isNewWidget: false,
isCalculationCompleted: isCalculationCompleted
? false
: ReportModel.isReportCalculationCompleted(reportWithData, report)
}, () => this.recalculateIfRequired());
}
}
};
async loadReport(reportId, optionalYouTrack) {
const fetchYouTrack = !optionalYouTrack
? fetcher().fetchYouTrack
: async (url, params) =>
await this.props.dashboardApi.fetch(optionalYouTrack.id, url, params);
try {
return await loadIssueDistributionReportWithData(
fetchYouTrack, reportId
);
} catch (err) {
this.setError(
DistributionReportsWidget.responseReportStatusToError(err.status)
);
return undefined;
}
}
loadReportWithAppliedConfigSettings =
async (reportId, optionalYouTrack) =>
DistributionReportsWidget.applyReportSettingsFromWidgetConfig(
await this.loadReport(reportId, optionalYouTrack),
DistributionReportsWidget.getConfigAsObject(this.props.configWrapper)
);
saveConfig = async () => {
const {report, refreshPeriod, youTrack} = this.state;
await this.props.configWrapper.replace({
reportId: report.id, youTrack, refreshPeriod
});
};
cancelConfig = async () => {
const {isNewWidget} = this.state;
if (isNewWidget) {
await this.props.dashboardApi.removeWidget();
} else {
this.setState({isConfiguring: false});
await this.props.dashboardApi.exitConfigMode();
this.initialize(this.props.dashboardApi);
}
};
openWidgetsSettings = () => {
this.props.dashboardApi.enterConfigMode();
this.setState({
isConfiguring: true,
isLoading: false
});
};
onChangeReportSortOrders =
async (mainAxisSortOrder, secondaryAxisSortOrder) => {
const {SortOrder} = DistributionReportAxises;
const {report} = this.state;
SortOrder.setMainAxisSortOrder(report, mainAxisSortOrder);
SortOrder.setSecondaryAxisSortOrder(report, secondaryAxisSortOrder);
this.setState({report});
if (this.props.editable) {
return SortOrder.isEditable(report)
? await saveReportSettings(fetcher().fetchYouTrack, report, true)
: await this.props.configWrapper.update({
reportId: report.id, mainAxisSortOrder, secondaryAxisSortOrder
});
}
return null;
};
onChangeReportPresentation = async presentation => {
const {report} = this.state;
report.presentation = presentation;
this.setState({report});
if (this.props.editable) {
return report.editable
? await saveReportSettings(fetcher().fetchYouTrack, report, true)
: await this.props.configWrapper.update({
reportId: report.id, presentation
});
}
return null;
};
renderConfigurationForm() {
const submitForm = async (selectedReportId, refreshPeriod, youTrack) => {
// eslint-disable-next-line react/no-access-state-in-setstate
const reportIsChanged = selectedReportId !== (this.state.report || {}).id;
this.setState({
youTrack,
isLoading: reportIsChanged,
isConfiguring: false,
// eslint-disable-next-line react/no-access-state-in-setstate
report: reportIsChanged ? null : this.state.report,
error: ReportModel.ErrorTypes.OK
}, async () => {
const reportWithData = await this.loadReportWithAppliedConfigSettings(
selectedReportId, youTrack
);
if (reportWithData) {
this.setState({
report: reportWithData,
isLoading: false,
isNewWidget: false,
refreshPeriod
}, async () => await Promise.all([
this.recalculateIfRequired(), this.saveConfig()
]));
}
});
};
const {
report, refreshPeriod, youTrack
} = this.state;
return (
<Configuration
reportId={(report || {}).id}
refreshPeriod={refreshPeriod}
onSubmit={submitForm}
onCancel={this.cancelConfig}
onGetReportDraft={ReportModel.NewReport.issueDistribution}
dashboardApi={this.props.dashboardApi}
youTrackId={(youTrack || {}).id}
/>
);
}
renderContent() {
const {
report,
error,
isLoading,
refreshPeriod,
youTrack,
isCalculationCompleted
} = this.state;
const isCalculation = ReportModel.isReportCalculation(report);
const tickPeriodSec = (isCalculation || isCalculationCompleted)
? DistributionReportsWidget.PROGRESS_BAR_REFRESH_PERIOD
: refreshPeriod;
const millisInSec = 1000;
if (isCalculationCompleted) {
const COMPLETED_PROGRESS = 100;
const shouldShowCompletedProgress =
report.status.progress < COMPLETED_PROGRESS &&
!ReportModel.isReportError(report);
if (shouldShowCompletedProgress) {
report.status.calculationInProgress = true;
report.status.progress = COMPLETED_PROGRESS;
}
}
return (
<Content
report={report}
error={error}
youTrack={youTrack}
dashboardApi={this.props.dashboardApi}
widgetLoader={isLoading || isCalculation}
tickPeriod={tickPeriodSec * millisInSec}
editable={this.props.editable}
onTick={this.onWidgetRefresh}
onOpenSettings={this.openWidgetsSettings}
onChangeReportSortOrders={this.onChangeReportSortOrders}
onChangePresentationMode={this.onChangeReportPresentation}
/>
);
}
render() {
const widgetTitle = this.state.isConfiguring
? DistributionReportsWidget.getDefaultWidgetTitle()
: DistributionReportsWidget.getPresentationModeWidgetTitle(
this.state.report, this.state.youTrack
);
const configuration = () => this.renderConfigurationForm();
const content = withWidgetLoaderHOC(() => this.renderContent());
return (
<div className="distribution-reports-widget">
<ConfigurableWidget
isConfiguring={this.state.isConfiguring}
dashboardApi={this.props.dashboardApi}
widgetTitle={widgetTitle}
Configuration={configuration}
Content={content}
/>
</div>
);
}
}
export default DistributionReportsWidget;