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

View File

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

View File

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