add export button

This commit is contained in:
Proddy
2022-04-14 18:25:50 +02:00
parent 2a4288e11d
commit cd5fef6891
3 changed files with 186 additions and 54 deletions

View File

@@ -24,10 +24,11 @@ import { useSnackbar } from 'notistack';
import { Table } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useSort, HeaderCellSort } from '@table-library/react-table-library/sort';
import { useSort } from '@table-library/react-table-library/sort';
import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useRowSelect } from '@table-library/react-table-library/select';
import DownloadIcon from '@mui/icons-material/GetApp';
import RefreshIcon from '@mui/icons-material/Refresh';
import EditIcon from '@mui/icons-material/Edit';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
@@ -98,8 +99,8 @@ const DashboardData: FC = () => {
}
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
font-size: 14px;
border-bottom: 1px solid #e0e0e0;
`,
Row: `
@@ -165,8 +166,9 @@ const DashboardData: FC = () => {
height: 32px;
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
font-size: 14px;
color: #90CAF9;
border-bottom: 1px solid #e0e0e0;
`,
Row: `
@@ -195,6 +197,7 @@ const DashboardData: FC = () => {
}
`,
BaseCell: `
padding-left: 8px;
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
@@ -202,9 +205,22 @@ const DashboardData: FC = () => {
text-align: right;
min-width: 64px;
}
`,
HeaderCell: `
padding-left: 0px;
`
});
const getSortIcon = (state: any, sortKey: any) => {
if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />;
}
if (state.sortKey === sortKey && !state.reverse) {
return <KeyboardArrowUpOutlinedIcon />;
}
};
const analog_sort = useSort(
{ nodes: sensorData.analogs },
{
@@ -222,7 +238,8 @@ const DashboardData: FC = () => {
sortFns: {
GPIO: (array) => array.sort((a, b) => a.i - b.i),
NAME: (array) => array.sort((a, b) => a.n.localeCompare(b.n)),
TYPE: (array) => array.sort((a, b) => a.t - b.t)
TYPE: (array) => array.sort((a, b) => a.t - b.t),
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
}
}
);
@@ -250,16 +267,21 @@ const DashboardData: FC = () => {
const dv_sort = useSort(
{ nodes: deviceData.data },
{},
{
state: {
sortKey: 'NAME',
reverse: false
}
},
{
sortIcon: {
margin: '0px',
iconDefault: null,
iconUp: <KeyboardArrowUpOutlinedIcon />,
iconDown: <KeyboardArrowDownOutlinedIcon />
},
sortFns: {
NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2)))
NAME: (array) => array.sort((a, b) => a.id.slice(2).localeCompare(b.id.slice(2))),
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
}
}
);
@@ -279,6 +301,52 @@ const DashboardData: FC = () => {
}
}
const escapeCsvCell = (cell: any) => {
if (cell == null) {
return '';
}
const sc = cell.toString().trim();
if (sc === '' || sc === '""') {
return sc;
}
if (sc.includes('"') || sc.includes(',') || sc.includes('\n') || sc.includes('\r')) {
return '"' + sc.replace(/"/g, '""') + '"';
}
return sc;
};
const makeCsvData = (columns: any, data: any) => {
return data.reduce((csvString: any, rowItem: any) => {
return csvString + columns.map(({ accessor }: any) => escapeCsvCell(accessor(rowItem))).join(',') + '\r\n';
}, columns.map(({ name }: any) => escapeCsvCell(name)).join(',') + '\r\n');
};
const downloadAsCsv = (columns: any, data: any, filename: string) => {
const csvData = makeCsvData(columns, data);
const csvFile = new Blob([csvData], { type: 'text/csv' });
const downloadLink = document.createElement('a');
downloadLink.download = filename;
downloadLink.href = window.URL.createObjectURL(csvFile);
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask;
const handleDownloadCsv = () => {
const columns = [
{ accessor: (dv: any) => dv.id.slice(2), name: 'Entity' },
{ accessor: (dv: any) => formatValue(dv.v, dv.u), name: 'Value' }
];
downloadAsCsv(
columns,
onlyFav ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) : deviceData.data,
'device_entities'
);
};
const refreshData = () => {
const selectedDevice = device_select.state.id;
if (selectedDevice === 'sensor') {
@@ -451,7 +519,7 @@ const DashboardData: FC = () => {
};
const addAnalogSensor = () => {
setAnalog({ id: '0', n: '', u: 0, v: 0, o: 0, t: 0, f: 1 });
setAnalog({ id: '0', i: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 });
};
const sendSensor = async () => {
@@ -585,8 +653,6 @@ const DashboardData: FC = () => {
return <FormLoader errorMessage={errorMessage} />;
}
console.log('** Rendering main data');
return (
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
{coreData.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
@@ -647,8 +713,6 @@ const DashboardData: FC = () => {
return;
}
const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask;
const sendCommand = (dv: DeviceValue) => {
if (dv.c && me.admin && !hasMask(dv.id, DeviceEntityMask.DV_READONLY)) {
setDeviceValue(dv);
@@ -672,8 +736,8 @@ const DashboardData: FC = () => {
</Typography>
<FormControlLabel
control={<Checkbox name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
label="show favorites only"
control={<Checkbox size="small" name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
label={<span style={{ fontSize: '12px' }}>favorites only</span>}
/>
<Table
@@ -689,10 +753,26 @@ const DashboardData: FC = () => {
<>
<Header>
<HeaderRow>
<HeaderCellSort resize sortKey="NAME">
ENTITY NAME
</HeaderCellSort>
<HeaderCell>VALUE</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(dv_sort.state, 'NAME')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
ENTITY NAME
</Button>
</HeaderCell>
<HeaderCell>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
VALUE
</Button>
</HeaderCell>
<HeaderCell />
</HeaderRow>
</Header>
@@ -745,10 +825,26 @@ const DashboardData: FC = () => {
<>
<Header>
<HeaderRow>
<HeaderCellSort resize sortKey="NAME">
NAME
</HeaderCellSort>
<HeaderCellSort sortKey="TEMPERATURE">TEMPERATURE</HeaderCellSort>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(sensor_sort.state, 'NAME')}
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
</Button>
</HeaderCell>
<HeaderCell>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(sensor_sort.state, 'TEMPERATURE')}
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'TEMPERATURE' })}
>
TEMPERATURE
</Button>
</HeaderCell>
<HeaderCell />
</HeaderRow>
</Header>
@@ -784,16 +880,46 @@ const DashboardData: FC = () => {
<>
<Header>
<HeaderRow>
<HeaderCellSort resize sortKey="GPIO">
GPIO
</HeaderCellSort>
<HeaderCellSort resize sortKey="NAME">
NAME
</HeaderCellSort>
<HeaderCellSort resize sortKey="TYPE">
TYPE
</HeaderCellSort>
<HeaderCell>VALUE</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
>
GPIO
</Button>
</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(analog_sort.state, 'NAME')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
</Button>
</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
>
TYPE
</Button>
</HeaderCell>
<HeaderCell>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
VALUE
</Button>
</HeaderCell>
<HeaderCell />
</HeaderRow>
</Header>
@@ -803,7 +929,7 @@ const DashboardData: FC = () => {
<Cell>{a.id}</Cell>
<Cell>{a.n}</Cell>
<Cell>{AnalogTypeNames[a.t]} </Cell>
<Cell>{formatValue(a.v, a.u)}</Cell>
<Cell>{a.t ? formatValue(a.v, a.u) : ''}</Cell>
<Cell>
{me.admin && (
<IconButton onClick={() => updateAnalog(a)}>
@@ -824,7 +950,7 @@ const DashboardData: FC = () => {
if (analog) {
try {
const response = await EMSESP.writeAnalog({
id: analog.id,
i: analog.i,
name: analog.n,
offset: analog.o,
factor: analog.f,
@@ -852,7 +978,7 @@ const DashboardData: FC = () => {
if (analog) {
try {
const response = await EMSESP.writeAnalog({
id: analog.id,
i: analog.i,
name: analog.n,
offset: analog.o,
factor: analog.f,
@@ -885,11 +1011,10 @@ const DashboardData: FC = () => {
<Grid container spacing={2}>
<Grid item>
<ValidatedTextField
name="id"
name="i"
label="GPIO"
value={analog.id}
// TODO add validation
// type="number"
value={analog.i}
type="number"
variant="outlined"
autoFocus
onChange={updateValue(setAnalog)}
@@ -1084,6 +1209,11 @@ const DashboardData: FC = () => {
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
Refresh
</Button>
{device_select.state.id && device_select.state.id !== 'sensor' && (
<Button startIcon={<DownloadIcon />} variant="outlined" onClick={handleDownloadCsv}>
Export
</Button>
)}
</ButtonRow>
</SectionContent>
);

View File

@@ -88,9 +88,10 @@ export interface Sensor {
}
export interface Analog {
id: string; // id string, is GPIO
id: string; // id string
i: number; // GPIO
n: string;
v?: number;
v: number; // is optional
u: number;
o: number;
f: number;
@@ -127,10 +128,10 @@ export interface Devices {
export interface DeviceValue {
id: string; // index, contains mask+name
v?: any; // value, in any format
v: any; // value, in any format
u: number; // uom
c?: string; // command
l?: string[]; // list
c?: string; // command, optional
l?: string[]; // list, optional
h?: string; // help text, optional
s?: string; // steps for up/down, optional
m?: string; // min, optional
@@ -277,7 +278,7 @@ export interface WriteValue {
}
export interface WriteAnalog {
id: string;
i: number;
name: string;
factor: number;
offset: number;

View File

@@ -410,10 +410,10 @@ const emsesp_sensordata = {
],
// sensors: [],
analogs: [
{ id: '36', n: 'motor', u: 0, o: 17, f: 0, t: 0 },
{ id: '37', n: 'External switch', v: 13, u: 0, o: 17, f: 0, t: 1 },
{ id: '39', n: 'Pulse count', v: 144, u: 0, o: 0, f: 0, t: 2 },
{ id: '40', n: 'Pressure', v: 16, u: 17, o: 0, f: 0, t: 3 },
{ id: '36', i: 36, n: 'motor', v: 0, u: 0, o: 17, f: 0, t: 0 },
{ id: '37', i: 37, n: 'External switch', v: 13, u: 0, o: 17, f: 0, t: 1 },
{ id: '39', i: 39, n: 'Pulse count', v: 144, u: 0, o: 0, f: 0, t: 2 },
{ id: '40', i: 40, n: 'Pressure', v: 16, u: 17, o: 0, f: 0, t: 3 },
],
// analogs: [],
}
@@ -485,7 +485,7 @@ const emsesp_devicedata_1 = {
const emsesp_devicedata_2 = {
label: 'Boiler: Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i',
data: [
{ u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] },
{ v: 0, u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] },
{ v: 'false', u: 0, id: '08heating active' },
{ v: 'false', u: 0, id: '04tapwater active' },
{ v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' },
@@ -872,7 +872,6 @@ rest_server.get(EMSESP_CORE_DATA_ENDPOINT, (req, res) => {
})
rest_server.get(EMSESP_SENSOR_DATA_ENDPOINT, (req, res) => {
console.log('send back sensor data...')
res.json(emsesp_sensordata)
})
rest_server.get(EMSESP_DEVICES_ENDPOINT, (req, res) => {
@@ -996,12 +995,13 @@ rest_server.post(EMSESP_WRITE_SENSOR_ENDPOINT, (req, res) => {
rest_server.post(EMSESP_WRITE_ANALOG_ENDPOINT, (req, res) => {
const analog = req.body
console.log('Write analog: ' + JSON.stringify(analog))
objIndex = emsesp_sensordata.analogs.findIndex((obj) => obj.id == analog.id)
objIndex = emsesp_sensordata.analogs.findIndex((obj) => obj.i == analog.i)
if (objIndex === -1) {
console.log('new analog')
emsesp_sensordata.analogs.push({
id: analog.id,
id: analog.i.toString(),
i: analog.i,
n: analog.name,
f: analog.factor,
o: analog.offset,
@@ -1010,9 +1010,10 @@ rest_server.post(EMSESP_WRITE_ANALOG_ENDPOINT, (req, res) => {
})
} else {
if (analog.type === -1) {
console.log('removing analog ' + analog.id)
console.log('removing analog ' + analog.i)
emsesp_sensordata.analogs[objIndex].t = -1
} else {
console.log('updating analog ' + analog.i)
emsesp_sensordata.analogs[objIndex].n = analog.name
emsesp_sensordata.analogs[objIndex].o = analog.offset
emsesp_sensordata.analogs[objIndex].f = analog.factor