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
418 lines
12 KiB
TypeScript
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);
|