frontend/src/component/ExportDataSettings.tsx
csawatzky 23223aa1a8 updated the api to use a new variable for whether to load all nodes, this will still use the show errors boolean for the component page
fixed issue in the filter function in the grain cable model that caused it to return empty when a top node was not set
updated the bin svg to have the disabled nodes greyed out
2025-07-31 13:20:31 -06:00

418 lines
12 KiB
TypeScript

import {
Button,
CircularProgress,
createStyles,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Grid2 as Grid,
InputAdornment,
TextField,
Typography,
Theme
} from "@mui/material"
import { withStyles, WithStyles } from "@mui/styles";
import DateSelect from "common/time/DateSelect";
import { Component, Device, User } from "models";
import moment, { Moment } from "moment";
import { ComponentMeasurement, getMeasurements } from "pbHelpers/ComponentType";
import { pond } from "protobuf-ts/pond";
import React from "react";
import { downloadJSON, exportDataToCSV } from "utils/download";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { ComponentAPIContext } from "providers/pond/componentAPI";
import { UnitMeasurement } from "models/UnitMeasurement";
const styles = (theme: Theme) =>
createStyles({
loadingContent: {
marginTop: theme.spacing(4)
}
});
interface Props extends WithStyles<typeof styles> {
device: Device;
component: Component;
isDialogOpen: boolean;
closeDialogCallback: Function;
initialStartDate?: Moment;
initialEndDate?: Moment;
isJSON?: boolean;
newMeasurements?: boolean;
user?: User;
as?: string
}
interface State {
startDate: any;
endDate: any;
isLoading: boolean;
filename: string;
}
class ExportDataSettings extends React.Component<Props, State> {
static contextType = ComponentAPIContext;
declare context: React.ContextType<typeof ComponentAPIContext>;
constructor(props: Props) {
super(props);
this.state = this.defaultSettings(props);
}
componentDidUpdate = (prevProps: Props) => {
const { initialStartDate, initialEndDate } = this.props;
if (
prevProps.initialStartDate !== initialStartDate ||
prevProps.initialEndDate !== initialEndDate
) {
this.setState({
startDate: initialStartDate,
endDate: initialEndDate
});
}
};
defaultSettings = (props: Props) => {
const { initialStartDate, initialEndDate } = props;
return {
startDate: initialStartDate ? initialStartDate : moment().subtract(1, "days"),
endDate: initialEndDate ? initialEndDate : moment(),
isLoading: false,
filename: moment()
.utc()
.format()
} as State;
};
close = () => {
this.setState(this.defaultSettings(this.props));
this.props.closeDialogCallback();
};
submit = () => {
const { device, component, user, as } = this.props;
const { startDate, endDate } = this.state;
const { /*sampleMeasurements*/ listUnitMeasurements } = this.context;
this.setState({ isLoading: true });
// if (newMeasurements) {
listUnitMeasurements(
device.id(),
component.key(),
startDate,
endDate,
0,
0,
"desc",
undefined,
[device.id().toString()],
["device"],
undefined,
undefined,
as
)
.then(resp => {
let measurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
this.formatUnitMeasurements(measurements)
.then((data: Array<any>) => {
this.setState({
isLoading: false
});
this.exportData(
data
.map(entry => {
let out = entry;
out.Timestamp = moment(entry.Timestamp)
.local()
.format();
return out;
})
.sort(function(a, b) {
return a.Timestamp < b.Timestamp ? -1 : 1;
})
);
this.close();
})
.catch((error: any) => {
this.setState({ isLoading: false });
this.close();
});
})
.catch((error: any) => {
this.setState({ isLoading: false });
this.close();
});
// } else {
// sampleMeasurements(device.id(), component.key(), startDate, endDate, 4294967295)
// .then((response: any) => {
// this.formatMeasurements(response.data.measurements)
// .then((data: Array<any>) => {
// this.setState({
// isLoading: false
// });
// this.exportData(
// data
// .map(entry => {
// let out = entry;
// out.Timestamp = moment(entry.Timestamp)
// .local()
// .format();
// return out;
// })
// .sort(function(a, b) {
// return a.Timestamp < b.Timestamp ? -1 : 1;
// })
// );
// this.close();
// })
// .catch((error: any) => {
// this.setState({ isLoading: false });
// this.close();
// });
// })
// .catch((error: any) => {
// this.setState({ isLoading: false });
// this.close();
// });
// }
};
// formatExportedMeasurements(measurements: Array<pond.ExportedMeasurement>): Promise<Array<any>> {
// return new Promise(function(resolve, reject) {
// if (!measurements) reject("Invalid measurements");
// let formattedMeasurements: Array<any> = [];
// for (var i = 0; i < measurements.length; i++) {
// let measurement: pond.ExportedMeasurement = measurements[i];
// let lastMeasurement = i === measurements.length - 1;
// if (!measurement || !measurement.timestamp) {
// if (lastMeasurement) {
// resolve(formattedMeasurements);
// }
// } else {
// let row: any = {
// Timestamp: measurement.timestamp,
// MeasurementType: measurement.measurementType,
// MeasurementValue: measurement.measurementValue,
// MeasurementUnit: measurement.measurementUnit
// };
// formattedMeasurements.push(row);
// if (lastMeasurement) {
// resolve(formattedMeasurements);
// }
// }
// }
// });
// }
formatMeasurements(measurements: Array<pond.Measurement>): Promise<Array<any>> {
const { device, component } = this.props;
return new Promise(function(resolve, reject) {
if (!measurements) reject("Invalid measurements");
let formattedMeasurements: Array<any> = [];
for (var i = 0; i < measurements.length; i++) {
let measurement: pond.Measurement = measurements[i];
let lastMeasurement = i === measurements.length - 1;
if (!measurement || !measurement.timestamp || !measurement.measurement) {
if (lastMeasurement) {
resolve(formattedMeasurements);
}
} else {
let row: any = {
Device: device.name(),
Component: component.name(),
Timestamp: measurement.timestamp
};
const componentMeasurements: Array<ComponentMeasurement> = getMeasurements(
component.settings.type
);
componentMeasurements.forEach((componentMeasurement: ComponentMeasurement) => {
let unit = describeMeasurement(
componentMeasurement.measurementType,
component.settings.type,
component.settings.subtype
).unit();
let key = componentMeasurement.label + " (" + unit + ")";
row[key] = componentMeasurement.extract(measurement.measurement);
});
formattedMeasurements.push(row);
if (lastMeasurement) {
resolve(formattedMeasurements);
}
}
}
});
}
formatUnitMeasurements(unitMeasurements: UnitMeasurement[]): Promise<Array<any>> {
const { device, component } = this.props;
return new Promise(function(resolve, reject) {
if (!unitMeasurements) reject("Invalid measurements");
let formattedMeasurements: Map<string, any> = new Map<string, any>();
unitMeasurements.forEach(um => {
let describer = describeMeasurement(
um.type,
component.settings.type,
component.settings.subtype
);
um.values.forEach((vals, i) => {
let timestamp = um.timestamps[i];
let row: any = formattedMeasurements.get(timestamp);
if (row === undefined) {
row = {
Device: device.name(),
Component: component.name(),
Timestamp: timestamp
};
}
let key = describer.label() + "(" + describer.unit() + ")";
let valueObj: any = {};
vals.values.forEach((val, i) => {
valueObj["node" + i] = val;
});
row[key] = valueObj;
formattedMeasurements.set(timestamp, row);
});
});
resolve(Array.from(formattedMeasurements.values()));
});
}
exportJSON(data: Array<any>) {
const { filename } = this.state;
downloadJSON(data, filename + ".json");
}
exportData(data: Array<any>) {
const { filename } = this.state;
exportDataToCSV(filename, data);
}
updateDateRange = (newStartDate: any, newEndDate: any) => {
this.setState({ startDate: newStartDate, endDate: newEndDate });
};
updateFilename = (event: any) => {
this.setState({ filename: event.target.value });
};
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
title() {
const { component } = this.props;
return (
<React.Fragment>
Data Export Settings
<Typography variant="body2" color="textSecondary">
{component.name()}
</Typography>
</React.Fragment>
);
}
content() {
const { classes } = this.props;
const { startDate, endDate, filename, isLoading } = this.state;
if (isLoading) {
return (
<Grid
container
direction="column"
alignItems="center"
className={classes.loadingContent}>
<CircularProgress color="secondary" />
<Typography variant="subtitle1" color="textPrimary">
Generating a custom {this.props.isJSON ? "json..." : "csv..."}
</Typography>
<Typography variant="subtitle2" color="textSecondary">
(This may take a while, please wait)
</Typography>
</Grid>
);
}
return (
<Grid container direction="row" spacing={2}>
<Grid size={12}>
<TextField
id="exportDataFilename"
label="Filename"
value={filename}
fullWidth
variant="standard"
onChange={this.updateFilename}
margin="normal"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{this.props.isJSON ? ".json" : ".csv"}
</InputAdornment>
)
}}
/>
</Grid>
<Grid size={12}>
<DateSelect
startDate={startDate}
endDate={endDate}
updateDateRange={this.updateDateRange}
label="Extract data from"
/>
</Grid>
<Grid size={12}>
<Typography variant="body2" color="textSecondary">
Note: This process might take a while (depends on how much data is requested)
</Typography>
</Grid>
</Grid>
);
}
actions() {
const { isLoading } = this.state;
return (
<React.Fragment>
<span>
<Button onClick={this.close} color="primary">
Cancel
</Button>
<Button onClick={this.submit} color="primary" disabled={isLoading}>
Export
</Button>
</span>
</React.Fragment>
);
}
render() {
return (
<Dialog
fullWidth
open={this.props.isDialogOpen}
onClose={this.close}
aria-labelledby="exportDataSettingsDialog">
<DialogTitle id="exportDataSettingsTitle">{this.title()}</DialogTitle>
<Divider />
<DialogContent>{this.content()}</DialogContent>
<DialogActions>{this.actions()}</DialogActions>
</Dialog>
);
}
}
export default withStyles(styles)(ExportDataSettings);