diff --git a/changelog.d/20240426_143800_boris_manual_analytics_report.md b/changelog.d/20240426_143800_boris_manual_analytics_report.md new file mode 100644 index 000000000000..e616757cca37 --- /dev/null +++ b/changelog.d/20240426_143800_boris_manual_analytics_report.md @@ -0,0 +1,4 @@ +### Changed + +- Analytics reports calculation may be initiated manually instead of automatic scheduling + () diff --git a/cvat-core/src/analytics-report.ts b/cvat-core/src/analytics-report.ts index 4d3637c7ad67..895ecb128c2e 100644 --- a/cvat-core/src/analytics-report.ts +++ b/cvat-core/src/analytics-report.ts @@ -1,9 +1,10 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { - SerializedAnalyticsEntry, SerializedAnalyticsReport, SerializedDataEntry, SerializedTransformationEntry, + SerializedAnalyticsEntry, SerializedAnalyticsReport, + SerializedDataEntry, SerializedTransformationEntry, } from './server-response-types'; import { ArgumentError } from './exceptions'; @@ -126,7 +127,7 @@ export default class AnalyticsReport { #statistics: AnalyticsEntry[]; constructor(initialData: SerializedAnalyticsReport) { - this.#id = initialData.id; + this.#id = initialData.job_id || initialData.task_id || initialData.project_id; this.#target = initialData.target as AnalyticsReportTarget; this.#createdDate = initialData.created_date; this.#statistics = []; diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 331ea9e5d8da..b9d3d1f450d5 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -29,7 +29,10 @@ import CloudStorage from './cloud-storage'; import Organization, { Invitation } from './organization'; import Webhook from './webhook'; import { ArgumentError } from './exceptions'; -import { SerializedAsset } from './server-response-types'; +import { + AnalyticsReportFilter, QualityConflictsFilter, QualityReportsFilter, + QualitySettingsFilter, SerializedAsset, +} from './server-response-types'; import QualityReport from './quality-report'; import QualityConflict, { ConflictSeverity } from './quality-conflict'; import QualitySettings from './quality-settings'; @@ -403,7 +406,7 @@ export default function implementAPI(cvat: CVATCore): CVATCore { return webhooks; }); - implementationMixin(cvat.analytics.quality.reports, async (filter) => { + implementationMixin(cvat.analytics.quality.reports, async (filter: QualityReportsFilter) => { checkFilter(filter, { page: isInteger, pageSize: isPageSize, @@ -426,7 +429,7 @@ export default function implementAPI(cvat: CVATCore): CVATCore { ); return reports; }); - implementationMixin(cvat.analytics.quality.conflicts, async (filter) => { + implementationMixin(cvat.analytics.quality.conflicts, async (filter: QualityConflictsFilter) => { checkFilter(filter, { reportID: isInteger, }); @@ -502,7 +505,7 @@ export default function implementAPI(cvat: CVATCore): CVATCore { return mergedConflicts; }); - implementationMixin(cvat.analytics.quality.settings.get, async (filter) => { + implementationMixin(cvat.analytics.quality.settings.get, async (filter: QualitySettingsFilter) => { checkFilter(filter, { taskID: isInteger, }); @@ -512,7 +515,7 @@ export default function implementAPI(cvat: CVATCore): CVATCore { const settings = await serverProxy.analytics.quality.settings.get(params); return new QualitySettings({ ...settings }); }); - implementationMixin(cvat.analytics.performance.reports, async (filter) => { + implementationMixin(cvat.analytics.performance.reports, async (filter: AnalyticsReportFilter) => { checkFilter(filter, { jobID: isInteger, taskID: isInteger, @@ -527,9 +530,30 @@ export default function implementAPI(cvat: CVATCore): CVATCore { const reportData = await serverProxy.analytics.performance.reports(params); return new AnalyticsReport(reportData); }); + implementationMixin(cvat.analytics.performance.calculate, async ( + body: Parameters[0], + onUpdate: Parameters[1], + ) => { + checkFilter(body, { + jobID: isInteger, + taskID: isInteger, + projectID: isInteger, + }); + + checkExclusiveFields(body, ['jobID', 'taskID', 'projectID'], []); + if (!('jobID' in body || 'taskID' in body || 'projectID' in body)) { + throw new ArgumentError('One of "jobID", "taskID", "projectID" is required, but not provided'); + } + + const params = fieldsToSnakeCase(body); + await serverProxy.analytics.performance.calculate(params, onUpdate); + }); implementationMixin(cvat.frames.getMeta, async (type, id) => { const result = await serverProxy.frames.getMeta(type, id); - return new FramesMetaData({ ...result }); + return new FramesMetaData({ + ...result, + deleted_frames: Object.fromEntries(result.deleted_frames.map((_frame) => [_frame, true])), + }); }); return cvat; diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index d4c6a21e9bf2..20e9ce8577fa 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -348,6 +348,14 @@ function build(): CVATCore { const result = await PluginRegistry.apiWrapper(cvat.analytics.performance.reports, filter); return result; }, + async calculate(body, onUpdate) { + const result = await PluginRegistry.apiWrapper( + cvat.analytics.performance.calculate, + body, + onUpdate, + ); + return result; + }, }, quality: { async reports(filter = {}) { diff --git a/cvat-core/src/index.ts b/cvat-core/src/index.ts index 9db3b5fca9e1..402ea4a69d9b 100644 --- a/cvat-core/src/index.ts +++ b/cvat-core/src/index.ts @@ -105,11 +105,11 @@ export default interface CVATCore { projects: { get: ( filter: { - id: number; - page: number; - search: string; - sort: string; - filter: string; + id?: number; + page?: number; + search?: string; + sort?: string; + filter?: string; } ) => Promise>; searchNames: any; @@ -141,6 +141,10 @@ export default interface CVATCore { }; performance: { reports: (filter: AnalyticsReportFilter) => Promise; + calculate: ( + body: { jobID?: number; taskID?: number; projectID?: number; }, + onUpdate: (status: enums.RQStatus, progress: number, message: string) => void, + ) => Promise; }; }; frames: { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 6d9e9f118021..228f467490f6 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -16,8 +16,8 @@ import { SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, SerializedAPISchema, SerializedInvitationData, SerializedCloudStorage, SerializedFramesMetaData, SerializedCollection, - SerializedQualitySettingsData, ApiQualitySettingsFilter, SerializedQualityConflictData, ApiQualityConflictsFilter, - SerializedQualityReportData, ApiQualityReportsFilter, SerializedAnalyticsReport, ApiAnalyticsReportFilter, + SerializedQualitySettingsData, APIQualitySettingsFilter, SerializedQualityConflictData, APIQualityConflictsFilter, + SerializedQualityReportData, APIQualityReportsFilter, SerializedAnalyticsReport, APIAnalyticsReportFilter, } from './server-response-types'; import { PaginatedResource } from './core-types'; import { Storage } from './storage'; @@ -1172,18 +1172,20 @@ async function restoreProject(storage: Storage, file: File | string) { return wait(); } -const listenToCreateCallbacks: Record; +type LongProcessListener = Record; onUpdate: ((state: string, progress: number, message: string) => void)[]; -}> = {}; +}>; + +const listenToCreateTaskCallbacks: LongProcessListener = {}; function listenToCreateTask( id, onUpdate: (state: RQStatus, progress: number, message: string) => void, ): Promise { - if (id in listenToCreateCallbacks) { - listenToCreateCallbacks[id].onUpdate.push(onUpdate); + if (id in listenToCreateTaskCallbacks) { + listenToCreateTaskCallbacks[id].onUpdate.push(onUpdate); // to avoid extra status check requests we do not create any more promises - return listenToCreateCallbacks[id].promise; + return listenToCreateTaskCallbacks[id].promise; } const promise = new Promise((resolve, reject) => { @@ -1195,7 +1197,7 @@ function listenToCreateTask( const state = response.data.state?.toLowerCase(); if ([RQStatus.QUEUED, RQStatus.STARTED].includes(state)) { // notify all the subscribtions when data status changed - listenToCreateCallbacks[id].onUpdate.forEach((callback) => { + listenToCreateTaskCallbacks[id].onUpdate.forEach((callback) => { callback( state, response.data.progress || 0, @@ -1210,14 +1212,14 @@ function listenToCreateTask( resolve(createdTask); } else if (state === RQStatus.FAILED) { const failMessage = 'Images processing failed'; - listenToCreateCallbacks[id].onUpdate.forEach((callback) => { + listenToCreateTaskCallbacks[id].onUpdate.forEach((callback) => { callback(state, 0, failMessage); }); reject(new ServerError(filterPythonTraceback(response.data.message), 400)); } else { const failMessage = 'Unknown status received'; - listenToCreateCallbacks[id].onUpdate.forEach((callback) => { + listenToCreateTaskCallbacks[id].onUpdate.forEach((callback) => { callback(state || RQStatus.UNKNOWN, 0, failMessage); }); reject( @@ -1228,7 +1230,7 @@ function listenToCreateTask( ); } } catch (errorData) { - listenToCreateCallbacks[id].onUpdate.forEach((callback) => { + listenToCreateTaskCallbacks[id].onUpdate.forEach((callback) => { callback('failed', 0, 'Server request failed'); }); reject(generateError(errorData)); @@ -1238,13 +1240,13 @@ function listenToCreateTask( setTimeout(checkStatus, 100); }); - listenToCreateCallbacks[id] = { + listenToCreateTaskCallbacks[id] = { promise, onUpdate: [onUpdate], }; promise.catch(() => { // do nothing, avoid uncaught promise exceptions - }).finally(() => delete listenToCreateCallbacks[id]); + }).finally(() => delete listenToCreateTaskCallbacks[id]); return promise; } @@ -2324,7 +2326,7 @@ async function createAsset(file: File, guideId: number): Promise { const { backendAPI } = config; @@ -2360,7 +2362,7 @@ async function updateQualitySettings( } async function getQualityConflicts( - filter: ApiQualityConflictsFilter, + filter: APIQualityConflictsFilter, ): Promise { const params = enableOrganization(); const { backendAPI } = config; @@ -2378,7 +2380,7 @@ async function getQualityConflicts( } async function getQualityReports( - filter: ApiQualityReportsFilter, + filter: APIQualityReportsFilter, ): Promise> { const { backendAPI } = config; @@ -2397,7 +2399,7 @@ async function getQualityReports( } async function getAnalyticsReports( - filter: ApiAnalyticsReportFilter, + filter: APIAnalyticsReportFilter, ): Promise { const { backendAPI } = config; @@ -2414,6 +2416,86 @@ async function getAnalyticsReports( } } +const listenToCreateAnalyticsReportCallbacks: { + job: LongProcessListener; + task: LongProcessListener; + project: LongProcessListener; +} = { + job: {}, + task: {}, + project: {}, +}; + +async function calculateAnalyticsReport( + body: { + job_id?: number; + task_id?: number; + project_id?: number; + }, + onUpdate: (state: string, progress: number, message: string) => void, +): Promise { + const id = body.job_id || body.task_id || body.project_id; + const { backendAPI } = config; + const params = enableOrganization(); + let listenerStorage: LongProcessListener = null; + + if (Number.isInteger(body.job_id)) { + listenerStorage = listenToCreateAnalyticsReportCallbacks.job; + } else if (Number.isInteger(body.task_id)) { + listenerStorage = listenToCreateAnalyticsReportCallbacks.task; + } else if (Number.isInteger(body.project_id)) { + listenerStorage = listenToCreateAnalyticsReportCallbacks.project; + } + + if (listenerStorage[id]) { + listenerStorage[id].onUpdate.push(onUpdate); + return listenerStorage[id].promise; + } + + const promise = new Promise((resolve, reject) => { + Axios.post(`${backendAPI}/analytics/reports`, { + ...body, + ...params, + }).then(({ data: { rq_id: rqID } }) => { + listenerStorage[id].onUpdate.forEach((_onUpdate) => _onUpdate(RQStatus.QUEUED, 0, 'Analytics report request sent')); + const checkStatus = (): void => { + Axios.post(`${backendAPI}/analytics/reports`, { + ...body, + ...params, + }, { params: { rq_id: rqID } }).then((response) => { + // TODO: rewrite server logic, now it returns 202, 201 codes, but we need RQ statuses and details + // after this patch is merged https://github.com/cvat-ai/cvat/pull/7537 + if (response.status === 201) { + listenerStorage[id].onUpdate.forEach((_onUpdate) => _onUpdate(RQStatus.FINISHED, 0, 'Done')); + resolve(); + return; + } + + listenerStorage[id].onUpdate.forEach((_onUpdate) => _onUpdate(RQStatus.QUEUED, 0, 'Analytics report calculation is in progress')); + setTimeout(checkStatus, 10000); + }).catch((errorData) => { + reject(generateError(errorData)); + }); + }; + + setTimeout(checkStatus, 2500); + }).catch((errorData) => { + reject(generateError(errorData)); + }); + }); + + listenerStorage[id] = { + promise, + onUpdate: [onUpdate], + }; + + promise.finally(() => { + delete listenerStorage[id]; + }); + + return promise; +} + export default Object.freeze({ server: Object.freeze({ setAuthData, @@ -2565,6 +2647,7 @@ export default Object.freeze({ analytics: Object.freeze({ performance: Object.freeze({ reports: getAnalyticsReports, + calculate: calculateAnalyticsReport, }), quality: Object.freeze({ reports: getQualityReports, diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 1894b17078f5..0bcce7cfb67e 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -26,7 +26,7 @@ export interface SerializedAnnotationFormats { exporters: SerializedAnnotationExporter[]; } -export interface ApiCommonFilterParams { +export interface APICommonFilterParams { page?: number; page_size?: number | 'all'; filter?: string; @@ -36,7 +36,7 @@ export interface ApiCommonFilterParams { search?: string; } -export interface ProjectsFilter extends ApiCommonFilterParams { +export interface ProjectsFilter extends APICommonFilterParams { id?: number; } @@ -233,10 +233,10 @@ export interface SerializedOrganization { contact?: SerializedOrganizationContact, } -export interface ApiQualitySettingsFilter extends ApiCommonFilterParams { +export interface APIQualitySettingsFilter extends APICommonFilterParams { task_id?: number; } -export type QualitySettingsFilter = Camelized; +export type QualitySettingsFilter = Camelized; export interface SerializedQualitySettingsData { id?: number; @@ -255,10 +255,10 @@ export interface SerializedQualitySettingsData { compare_attributes?: boolean; } -export interface ApiQualityConflictsFilter extends ApiCommonFilterParams { +export interface APIQualityConflictsFilter extends APICommonFilterParams { report_id?: number; } -export type QualityConflictsFilter = Camelized; +export type QualityConflictsFilter = Camelized; export interface SerializedAnnotationConflictData { job_id?: number; @@ -279,14 +279,14 @@ export interface SerializedQualityConflictData { description?: string; } -export interface ApiQualityReportsFilter extends ApiCommonFilterParams { +export interface APIQualityReportsFilter extends APICommonFilterParams { parent_id?: number; peoject_id?: number; task_id?: number; job_id?: number; target?: string; } -export type QualityReportsFilter = Camelized; +export type QualityReportsFilter = Camelized; export interface SerializedQualityReportData { id?: number; @@ -345,17 +345,19 @@ export interface SerializedAnalyticsEntry { transformations?: SerializedTransformationEntry[]; } -export interface ApiAnalyticsReportFilter extends ApiCommonFilterParams { +export interface APIAnalyticsReportFilter { project_id?: number; task_id?: number; job_id?: number; start_date?: string; end_date?: string; } -export type AnalyticsReportFilter = Camelized; +export type AnalyticsReportFilter = Camelized; export interface SerializedAnalyticsReport { - id?: number; + job_id?: number; + task_id?: number; + project_id?: number; target?: string; created_date?: string; statistics?: SerializedAnalyticsEntry[]; diff --git a/cvat-ui/src/components/analytics-page/analytics-page.tsx b/cvat-ui/src/components/analytics-page/analytics-page.tsx index 8284130052f9..410fd3007f0b 100644 --- a/cvat-ui/src/components/analytics-page/analytics-page.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-page.tsx @@ -13,7 +13,9 @@ import Title from 'antd/lib/typography/Title'; import notification from 'antd/lib/notification'; import { useIsMounted } from 'utils/hooks'; import { Project, Task } from 'reducers'; -import { AnalyticsReport, Job, getCore } from 'cvat-core-wrapper'; +import { + AnalyticsReport, Job, RQStatus, getCore, +} from 'cvat-core-wrapper'; import moment from 'moment'; import CVATLoadingSpinner from 'components/common/loading-spinner'; import GoBackButton from 'components/common/go-back-button'; @@ -53,106 +55,121 @@ function handleTimePeriod(interval: DateIntervals): [string, string] { } } +function readInstanceType(location: ReturnType): InstanceType { + if (location.pathname.includes('projects')) { + return 'project'; + } + if (location.pathname.includes('jobs')) { + return 'job'; + } + return 'task'; +} + +function readInstanceId(type: InstanceType): number { + if (type === 'project') { + return +useParams<{ pid: string }>().pid; + } + if (type === 'job') { + return +useParams<{ jid: string }>().jid; + } + return +useParams<{ tid: string }>().tid; +} + type InstanceType = 'project' | 'task' | 'job'; function AnalyticsPage(): JSX.Element { const location = useLocation(); - const requestedInstanceType: InstanceType = (() => { - if (location.pathname.includes('projects')) { - return 'project'; - } - if (location.pathname.includes('jobs')) { - return 'job'; - } - return 'task'; - })(); - - const requestedInstanceID: number = (() => { - if (requestedInstanceType === 'project') { - return +useParams<{ pid: string }>().pid; - } - if (requestedInstanceType === 'job') { - return +useParams<{ jid: string }>().jid; - } - return +useParams<{ tid: string }>().tid; - })(); + const requestedInstanceType: InstanceType = readInstanceType(location); + const requestedInstanceID = readInstanceId(requestedInstanceType); const [activeTab, setTab] = useState(getTabFromHash()); - const [instanceType, setInstanceType] = useState(null); const [instance, setInstance] = useState(null); const [analyticsReport, setAnalyticsReport] = useState(null); const [timePeriod, setTimePeriod] = useState(DateIntervals.LAST_WEEK); + const [reportRefreshingStatus, setReportRefreshingStatus] = useState(null); const [fetching, setFetching] = useState(true); const isMounted = useIsMounted(); - const receiveInstance = (type: InstanceType, id: number): Promise => { - if (type === 'project') { - return core.projects.get({ id }); - } + const receiveInstance = async (type: InstanceType, id: number): Promise => { + let receivedInstance: Task | Project | Job | null = null; - if (type === 'task') { - return core.tasks.get({ id }); - } + try { + switch (type) { + case 'project': { + [receivedInstance] = await core.projects.get({ id }); + break; + } + case 'task': { + [receivedInstance] = await core.tasks.get({ id }); + break; + } + case 'job': { + [receivedInstance] = await core.jobs.get({ jobID: id }); + break; + } + default: + return; + } - return core.jobs.get({ jobID: id }); + if (isMounted()) { + setInstance(receivedInstance); + setInstanceType(type); + } + } catch (error: unknown) { + notification.error({ + message: `Could not receive requested ${type}`, + description: `${error instanceof Error ? error.message : ''}`, + }); + } }; - const receiveReport = (timeInterval: DateIntervals, type: InstanceType, id: number): Promise => { + const receiveReport = async (timeInterval: DateIntervals, type: InstanceType, id: number): Promise => { const [endDate, startDate] = handleTimePeriod(timeInterval); - if (type === 'project') { - return core.analytics.performance.reports({ - projectID: id, - endDate, - startDate, - }); - } + let report: AnalyticsReport | null = null; + + try { + const body = { endDate, startDate }; + switch (type) { + case 'project': { + report = await core.analytics.performance.reports({ ...body, projectID: id }); + break; + } + case 'task': { + report = await core.analytics.performance.reports({ ...body, taskID: id }); + break; + } + case 'job': { + report = await core.analytics.performance.reports({ ...body, jobID: id }); + break; + } + default: + return; + } - if (type === 'task') { - return core.analytics.performance.reports({ - taskID: id, - endDate, - startDate, + if (isMounted()) { + setAnalyticsReport(report); + } + } catch (error: unknown) { + notification.error({ + message: 'Could not receive requested report', + description: `${error instanceof Error ? error.message : ''}`, }); } - - return core.analytics.performance.reports({ - jobID: id, - endDate, - startDate, - }); }; useEffect(() => { - setFetching(true); - if (Number.isInteger(requestedInstanceID) && ['project', 'task', 'job'].includes(requestedInstanceType)) { + setFetching(true); Promise.all([ receiveInstance(requestedInstanceType, requestedInstanceID), receiveReport(timePeriod, requestedInstanceType, requestedInstanceID), - ]) - .then(([instanceResponse, report]) => { - const receivedInstance: Task | Project | Job = instanceResponse[0]; - if (receivedInstance && isMounted()) { - setInstance(receivedInstance); - setInstanceType(requestedInstanceType); - } - if (report && isMounted()) { - setAnalyticsReport(report); - } - }) - .catch((error: Error) => { - notification.error({ - message: 'Could not receive requested resources', - description: `${error.toString()}`, - }); - }) - .finally(() => { - if (isMounted()) { - setFetching(false); - } - }); + ]).finally(() => { + if (isMounted()) { + setFetching(false); + } + }); } else { notification.error({ message: 'Could not load this page', @@ -168,6 +185,42 @@ function AnalyticsPage(): JSX.Element { }; }, [requestedInstanceType, requestedInstanceID, timePeriod]); + useEffect(() => { + window.addEventListener('hashchange', () => { + const hash = getTabFromHash(); + setTab(hash); + }); + }, []); + + useEffect(() => { + window.location.hash = activeTab; + }, [activeTab]); + + const onCreateReport = useCallback(() => { + const onUpdate = (status: RQStatus, progress: number, message: string): void => { + setReportRefreshingStatus(message); + }; + + const body = { + ...(requestedInstanceType === 'project' ? { projectID: requestedInstanceID } : {}), + ...(requestedInstanceType === 'task' ? { taskID: requestedInstanceID } : {}), + ...(requestedInstanceType === 'job' ? { jobID: requestedInstanceID } : {}), + }; + + core.analytics.performance.calculate(body, onUpdate).then(() => { + receiveReport(timePeriod, requestedInstanceType, requestedInstanceID); + }).finally(() => { + setReportRefreshingStatus(null); + }).catch((error: unknown) => { + if (isMounted()) { + notification.error({ + message: 'Error occurred during requesting performance report', + description: error instanceof Error ? error.message : '', + }); + } + }); + }, [requestedInstanceType, requestedInstanceID, timePeriod]); + const onJobUpdate = useCallback((job: Job): void => { setFetching(true); @@ -185,20 +238,9 @@ function AnalyticsPage(): JSX.Element { }); }, []); - useEffect(() => { - window.addEventListener('hashchange', () => { - const hash = getTabFromHash(); - setTab(hash); - }); - }, []); - - const onTabKeyChange = (key: string): void => { + const onTabKeyChange = useCallback((key: string): void => { setTab(key as AnalyticsTabs); - }; - - useEffect(() => { - window.location.hash = activeTab; - }, [activeTab]); + }, []); let backNavigation: JSX.Element | null = null; let title: JSX.Element | null = null; @@ -238,7 +280,9 @@ function AnalyticsPage(): JSX.Element { {instanceType === 'task' && ( diff --git a/cvat-ui/src/components/analytics-page/analytics-performance.tsx b/cvat-ui/src/components/analytics-page/analytics-performance.tsx index 164504f7816c..afeab8ccbfd4 100644 --- a/cvat-ui/src/components/analytics-page/analytics-performance.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-performance.tsx @@ -8,8 +8,14 @@ import RGL, { WidthProvider } from 'react-grid-layout'; import Text from 'antd/lib/typography/Text'; import Select from 'antd/lib/select'; import Notification from 'antd/lib/notification'; -import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; import { Col, Row } from 'antd/lib/grid'; +import Button from 'antd/lib/button'; +import Card from 'antd/lib/card'; +import Title from 'antd/lib/typography/Title'; +import { ReloadOutlined, LoadingOutlined } from '@ant-design/icons'; + +import { AnalyticsReport, AnalyticsEntryViewType } from 'cvat-core-wrapper'; +import CVATTooltip from 'components/common/cvat-tooltip'; import HistogramView from './views/histogram-view'; import AnalyticsCard from './views/analytics-card'; @@ -25,7 +31,9 @@ export enum DateIntervals { interface Props { report: AnalyticsReport | null; timePeriod: DateIntervals; + reportRefreshingStatus: string | null; onTimePeriodChange: (val: DateIntervals) => void; + onCreateReport: () => void; } const colors = [ @@ -37,12 +45,44 @@ const colors = [ ]; function AnalyticsOverview(props: Props): JSX.Element | null { - const { report, timePeriod, onTimePeriodChange } = props; + const { + report, timePeriod, reportRefreshingStatus, + onTimePeriodChange, onCreateReport, + } = props; - if (!report) return null; const layout: any = []; let histogramCount = 0; let numericCount = 0; + + if (report === null) { + return null; + } + + if (!report.id) { + return ( +
+ + + +
+ {reportRefreshingStatus ? {reportRefreshingStatus} : + Performance report was not calculated yet... } + +
+
+ +
+
+ ); + } + const views: { view: React.JSX.Element, key: string }[] = []; report.statistics.forEach((entry) => { const tooltip = ( @@ -138,12 +178,21 @@ function AnalyticsOverview(props: Props): JSX.Element | null { } } }); + return (
+ +