|
|
@ -59,12 +59,19 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange); |
|
|
|
const [selectedTimeRange, setTimeRange] = useState<TimeRange>(timeRange); |
|
|
|
|
|
|
|
|
|
|
|
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING); |
|
|
|
const [panelStatus, setPanelStatus] = useState<PanelStatus>(PanelStatus.LOADING); |
|
|
|
|
|
|
|
function setPanelStatusWithValidate(status: PanelStatus): void { |
|
|
|
|
|
|
|
if (panelStatus === PanelStatus.PERMISSION_ERROR) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return setPanelStatus(status); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null); |
|
|
|
|
|
|
|
|
|
|
|
const timeZoneName = convertTimeZoneTypeToName(timeZone); |
|
|
|
const timeZoneName = convertTimeZoneTypeToName(timeZone); |
|
|
|
|
|
|
|
|
|
|
|
if (contextSrv.user.orgRole !== OrgRole.Admin) { |
|
|
|
if (contextSrv.user.orgRole !== OrgRole.Admin) { |
|
|
|
// TODO: it shouldn't be overriten
|
|
|
|
setPanelStatusWithValidate(PanelStatus.PERMISSION_ERROR); |
|
|
|
setPanelStatus(PanelStatus.PERMISSION_ERROR); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
@ -121,7 +128,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
setTasksDataFrame(dataFrame); |
|
|
|
setTasksDataFrame(dataFrame); |
|
|
|
}, [tasks]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
}, [tasks]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(refresh, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
useEffect(fetchTasks, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
if (queries === null) { |
|
|
|
if (queries === null) { |
|
|
@ -130,25 +137,26 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
setQueriesDataFrame(getDataFrameForQueriesTable(queries)); |
|
|
|
setQueriesDataFrame(getDataFrameForQueriesTable(queries)); |
|
|
|
}, [queries]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
}, [queries]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
|
|
|
|
function refresh(): void { |
|
|
|
function fetchTasks(): void { |
|
|
|
getTasks() |
|
|
|
getTasks() |
|
|
|
.then((tasks) => { |
|
|
|
.then((tasks) => { |
|
|
|
setTasks(tasks); |
|
|
|
setTasks(tasks); |
|
|
|
setPanelStatus(PanelStatus.OK); |
|
|
|
setPanelStatusWithValidate(PanelStatus.OK); |
|
|
|
for (let task of tasks) { |
|
|
|
for (let task of tasks) { |
|
|
|
if (task.progress?.status === ExportStatus.EXPORTING) { |
|
|
|
if (task.progress?.status === ExportStatus.EXPORTING) { |
|
|
|
setTimeout(refresh, 1000); |
|
|
|
setTimeout(fetchTasks, 1000); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
.catch((err) => { |
|
|
|
.catch((err) => { |
|
|
|
setPanelStatus(PanelStatus.DATASOURCE_ERROR); |
|
|
|
setPanelStatusWithValidate(PanelStatus.DATASOURCE_ERROR); |
|
|
|
console.error('some error', err); |
|
|
|
console.error('some error', err); |
|
|
|
|
|
|
|
setErrorMessage(`${err.name}: ${err.message}`); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
eventBus.subscribe(RefreshEvent, refresh); |
|
|
|
eventBus.subscribe(RefreshEvent, fetchTasks); |
|
|
|
|
|
|
|
|
|
|
|
function getDatasourceByUid(uid?: string): DataSourceSettings | undefined { |
|
|
|
function getDatasourceByUid(uid?: string): DataSourceSettings | undefined { |
|
|
|
if (_.isNil(uid)) { |
|
|
|
if (_.isNil(uid)) { |
|
|
@ -191,7 +199,7 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
}, |
|
|
|
}, |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
refresh(); |
|
|
|
fetchTasks(); |
|
|
|
|
|
|
|
|
|
|
|
onCloseModal(); |
|
|
|
onCloseModal(); |
|
|
|
unselectAllQueries(); |
|
|
|
unselectAllQueries(); |
|
|
@ -271,48 +279,59 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { |
|
|
|
function getDataFrameForTaskTable(tasks: ExportTask[]): DataFrame { |
|
|
|
|
|
|
|
const sortedTasks = _.orderBy(tasks, (task) => task.progress?.time, 'desc'); |
|
|
|
const dataFrame = toDataFrame({ |
|
|
|
const dataFrame = toDataFrame({ |
|
|
|
name: 'A', |
|
|
|
name: 'A', |
|
|
|
fields: [ |
|
|
|
fields: [ |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Time', |
|
|
|
name: 'Time', |
|
|
|
type: FieldType.number, |
|
|
|
type: FieldType.number, |
|
|
|
values: _.map(tasks, (task) => convertTimestampToDate(task.progress?.time)), |
|
|
|
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.progress?.time)), |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
name: 'From', |
|
|
|
|
|
|
|
type: FieldType.number, |
|
|
|
|
|
|
|
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.from)), |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
name: 'To', |
|
|
|
|
|
|
|
type: FieldType.number, |
|
|
|
|
|
|
|
values: _.map(sortedTasks, (task) => convertTimestampToDate(task.timeRange.to)), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'User', |
|
|
|
name: 'User', |
|
|
|
type: FieldType.string, |
|
|
|
type: FieldType.string, |
|
|
|
values: _.map(tasks, (task) => task.username), |
|
|
|
values: _.map(sortedTasks, (task) => task.username), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Datasource', |
|
|
|
name: 'Datasource', |
|
|
|
type: FieldType.string, |
|
|
|
type: FieldType.string, |
|
|
|
values: _.map(tasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), |
|
|
|
values: _.map(sortedTasks, (task) => task.queries.map((query) => query.datasource?.name).join(',')), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Exported Rows', |
|
|
|
name: 'Exported Rows', |
|
|
|
type: FieldType.number, |
|
|
|
type: FieldType.number, |
|
|
|
values: _.map(tasks, (task) => task.progress?.exportedRowsCount), |
|
|
|
values: _.map(sortedTasks, (task) => task.progress?.exportedRowsCount), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Progress', |
|
|
|
name: 'Progress', |
|
|
|
type: FieldType.string, |
|
|
|
type: FieldType.string, |
|
|
|
values: _.map(tasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), |
|
|
|
values: _.map(sortedTasks, (task) => `${((task.progress?.progress || 0) * 100).toFixed(0)}%`), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Status', |
|
|
|
name: 'Status', |
|
|
|
type: FieldType.string, |
|
|
|
type: FieldType.string, |
|
|
|
values: _.map(tasks, (task) => task.progress?.status), |
|
|
|
values: _.map(sortedTasks, (task) => task.progress?.status), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Error', |
|
|
|
name: 'Error', |
|
|
|
type: FieldType.string, |
|
|
|
type: FieldType.string, |
|
|
|
values: _.map(tasks, (task) => task.progress?.errorMessage || '-'), |
|
|
|
values: _.map(sortedTasks, (task) => task.progress?.errorMessage || '-'), |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
{ |
|
|
|
name: 'Download CSV', |
|
|
|
name: 'Download CSV', |
|
|
|
type: FieldType.string, |
|
|
|
type: FieldType.string, |
|
|
|
values: _.map(tasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), |
|
|
|
values: _.map(sortedTasks, () => `data:image/png;base64,${DOWNLOAD_ICON_BASE_64}`), |
|
|
|
config: { |
|
|
|
config: { |
|
|
|
custom: { |
|
|
|
custom: { |
|
|
|
filterable: false, |
|
|
|
filterable: false, |
|
|
@ -396,15 +415,19 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
<div> |
|
|
|
<div> |
|
|
|
<p>Datasource is unavailable.</p> |
|
|
|
<p>Datasource is unavailable.</p> |
|
|
|
<div> |
|
|
|
<div> |
|
|
|
Click <a href={`/plugins/${APP_ID}`}>here</a> to configure DataExporter. |
|
|
|
If you have not setup the plugin click |
|
|
|
|
|
|
|
<a className={styles.customLink} href={`/plugins/${APP_ID}`}> |
|
|
|
|
|
|
|
here |
|
|
|
|
|
|
|
</a> |
|
|
|
|
|
|
|
to configure DataExporter. |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/* TODO: display error message? */} |
|
|
|
<p style={{ marginTop: '16px' }}>{errorMessage}</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
); |
|
|
|
); |
|
|
|
const permissionErrorDiv = ( |
|
|
|
const permissionErrorDiv = ( |
|
|
|
<div> |
|
|
|
<div> |
|
|
|
<p> Permission Error. </p> |
|
|
|
<p> Permission Error. </p> |
|
|
|
<div> DataExporter panel availabel only for Admins </div> |
|
|
|
<div> DataExporter panel available only for Admins. </div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
); |
|
|
|
); |
|
|
|
const mainDiv = ( |
|
|
|
const mainDiv = ( |
|
|
@ -422,8 +445,9 @@ export function Panel({ width, height, timeRange, eventBus, timeZone }: Props) { |
|
|
|
</Button> |
|
|
|
</Button> |
|
|
|
<Modal title="Select Queries" isOpen={isModalOpen} onDismiss={onCloseModal} className={styles.calendarModal}> |
|
|
|
<Modal title="Select Queries" isOpen={isModalOpen} onDismiss={onCloseModal} className={styles.calendarModal}> |
|
|
|
{queriesDataFrame === null ? ( |
|
|
|
{queriesDataFrame === null ? ( |
|
|
|
// TODO: if datasource responds with error, display the error
|
|
|
|
<div> |
|
|
|
<LoadingPlaceholder text="Loading..."></LoadingPlaceholder> |
|
|
|
<p>There are no queries to export.</p> |
|
|
|
|
|
|
|
</div> |
|
|
|
) : ( |
|
|
|
) : ( |
|
|
|
<div> |
|
|
|
<div> |
|
|
|
<VerticalGroup spacing="xs"> |
|
|
|
<VerticalGroup spacing="xs"> |
|
|
@ -480,4 +504,11 @@ const getStyles = () => ({ |
|
|
|
z-index: 1061; |
|
|
|
z-index: 1061; |
|
|
|
} |
|
|
|
} |
|
|
|
`,
|
|
|
|
`,
|
|
|
|
|
|
|
|
customLink: css` |
|
|
|
|
|
|
|
color: #6e9fff; |
|
|
|
|
|
|
|
margin: 0 4px; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
|
|
|
|
text-decoration: underline; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
`,
|
|
|
|
}); |
|
|
|
}); |
|
|
|