Skip to content

Commit

Permalink
Allow to filtering time of DataStream data
Browse files Browse the repository at this point in the history
Add from/to time input fields to filter Interface DataStream data

Signed-off-by: Armin Ahmetović <[email protected]>
  • Loading branch information
arahmarchak committed Jul 15, 2024
1 parent ff9fa61 commit dddf217
Showing 1 changed file with 171 additions and 19 deletions.
190 changes: 171 additions & 19 deletions src/DeviceInterfaceValues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button, Card, Col, Container, Form, Modal, Row, Spinner, Table } from 'react-bootstrap';
import DatePicker from 'react-datepicker';
import {
AstarteDataTuple,
AstarteDataTreeNode,
Expand All @@ -35,12 +36,10 @@ import _ from 'lodash';
import BackButton from './ui/BackButton';
import Empty from './components/Empty';
import WaitForData from './components/WaitForData';
import useFetch from './hooks/useFetch';
import { useAstarte } from './AstarteManager';
import { AlertsBanner, useAlerts } from 'AlertManager';
import * as yup from 'yup';
import { getValidationSchema } from 'astarte-client/models/InterfaceValue';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';

const MAX_SHOWN_VALUES = 20;
Expand Down Expand Up @@ -120,14 +119,16 @@ const ObjectDatastreamTable = ({
if (treeData.length === 0) {
return <p>No data sent by the device.</p>;
}

const objectProperties = Object.keys(treeData[0].value);
const orderedData = _.orderBy(treeData, 'timestamp', 'desc');

return (
<>
<h5 className="mb-1">Path</h5>
<p>{dataTreeNode.endpoint}</p>
<Row className="align-items-center mb-4">
<Col>
<h5 className="mb-1 mt-2">Path</h5>
<p>{dataTreeNode.endpoint}</p>
</Col>
</Row>
<Table responsive>
<thead>
<tr>
Expand Down Expand Up @@ -517,31 +518,143 @@ const SendInterfaceDataModal = ({
);
};

interface TimeRangeModalProps {
showTimeModal: boolean;
handleShowTimeModal: () => void;
requestingData: boolean;
fetchInterfaceTimeData: (since?: string, to?: string) => void;
}

const TimeRangeModal = ({
showTimeModal,
requestingData,
handleShowTimeModal,
fetchInterfaceTimeData,
}: TimeRangeModalProps) => {
const [fromTime, setFromTime] = useState<Date | null>(null);
const [toTime, setToTime] = useState<Date | null>(null);
const [loading, setLoading] = useState<boolean>(false);

const handleFetchData = async () => {
setLoading(true);
try {
const from = fromTime ? fromTime.toISOString() : new Date(0).toISOString();
const to = toTime ? toTime.toISOString() : new Date().toISOString();
fetchInterfaceTimeData(from, to);
handleShowTimeModal();
} catch (error) {
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
handleFetchData();
};

return (
<Modal size="lg" centered show={showTimeModal} onHide={handleShowTimeModal}>
<Form onSubmit={handleSubmit}>
<Modal.Header closeButton>
<Modal.Title>Select Time Range</Modal.Title>
</Modal.Header>
<Modal.Body>
<Container>
<Row className="align-items-center">
<Col className="mb-3">
<Form.Label className="m-2">From</Form.Label>
<DatePicker
selected={fromTime}
onChange={(date: Date) => setFromTime(date)}
showTimeSelect
dateFormat="Pp"
className="form-control"
placeholderText="Select from time"
/>
</Col>
<Col className="mb-3">
<Form.Label className="m-2">To</Form.Label>
<DatePicker
selected={toTime}
onChange={(date: Date) => setToTime(date)}
showTimeSelect
dateFormat="Pp"
className="form-control"
placeholderText="Select to time"
/>
</Col>
</Row>
</Container>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleShowTimeModal}>
Cancel
</Button>
<Button variant="primary" onClick={handleFetchData} disabled={requestingData || loading}>
{requestingData ? <Spinner size="sm" animation="border" /> : 'Filter Data'}
</Button>
</Modal.Footer>
</Form>
</Modal>
);
};

export default (): React.ReactElement => {

Check warning on line 604 in src/DeviceInterfaceValues.tsx

View workflow job for this annotation

GitHub Actions / Run code quality and funcionality tests

Assign arrow function to a variable before exporting as module default
const { deviceId = '', interfaceName = '' } = useParams();
const astarte = useAstarte();
const deviceDataFetcher = useFetch(() =>
astarte.client.getDeviceDataTree({
deviceId,
interfaceName,
}),
);
const [showModal, setShowModal] = useState(false);
const [showTimeModal, setShowTimeModal] = useState(false);
const [sendingData, setSendingData] = useState(false);
const [requestingData, setRequestingData] = useState(false);
const [filteredData, setFilteredData] =
useState<AstarteDataTreeNode<AstarteDatastreamObjectData>>();
const [formAlerts, formAlertsController] = useAlerts();
const iface = deviceDataFetcher.value?.interface as AstarteInterface;
const [data, setData] = useState<InterfaceDataProps['interfaceData'] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

const iface = data?.interface as AstarteInterface;

const handleShowModal = () => {
setShowModal(!showModal);
};

const handleShowTimeModal = () => {
setShowTimeModal(!showTimeModal);
};

const fetchInterfaceData = async (since?: string, to?: string) => {
setRequestingData(true);
astarte.client
.getDeviceDataTree({
deviceId,
interfaceName,
since,
to,
})
.then((data) => {
setFilteredData(data as AstarteDataTreeNode<AstarteDatastreamObjectData>);
handleShowTimeModal();
})
.catch((err) => {
formAlertsController.showError(
`Could not fetch data to interface: ${err.response.data.errors.detail}`,
);
handleShowModal();
})
.finally(() => {
setRequestingData(false);
});
};

const sendInterfaceData = (data: { endpoint: string; value: AstarteDataValue }) => {
setSendingData(true);
astarte.client
.sendDataToInterface({ deviceId, interfaceName, path: data.endpoint, data: data.value })
.then(() => {
handleShowModal();
deviceDataFetcher.refresh();
})
.catch((err) => {
formAlertsController.showError(
Expand All @@ -554,6 +667,23 @@ export default (): React.ReactElement => {
});
};

useEffect(() => {
const fetchDeviceData = async () => {
try {
setLoading(true);
setError(null);
const result = await astarte.client.getDeviceDataTree({ deviceId, interfaceName });
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};

fetchDeviceData();
}, [deviceId, interfaceName]);

Check warning on line 685 in src/DeviceInterfaceValues.tsx

View workflow job for this annotation

GitHub Actions / Run code quality and funcionality tests

React Hook useEffect has a missing dependency: 'astarte.client'. Either include it or remove the dependency array

return (
<Container fluid className="p-3">
<div className="d-flex justify-content-between">
Expand All @@ -566,11 +696,16 @@ export default (): React.ReactElement => {
'POST',
`devices/${deviceId}/interfaces/${interfaceName}`,
) &&
iface?.ownership === 'server' && (
data?.interface.ownership === 'server' && (
<Button onClick={handleShowModal} className="m-2">
Publish Data
</Button>
)}
{data?.interface.type !== 'properties' && (
<Button onClick={handleShowTimeModal} className="m-2">
Filter Data
</Button>
)}
</div>
<AlertsBanner alerts={formAlerts} />
<Card className="mt-4">
Expand All @@ -579,18 +714,27 @@ export default (): React.ReactElement => {
</Card.Header>
<Card.Body>
<WaitForData
data={deviceDataFetcher.value}
status={deviceDataFetcher.status}
data={data}
status={loading ? 'loading' : error ? 'err' : 'ok'}
fallback={
<Container fluid className="text-center">
<Spinner animation="border" role="status" />
</Container>
}
errorFallback={
<Empty title="Couldn't load interface data" onRetry={deviceDataFetcher.refresh} />
<Empty
title="Couldn't load interface data"
onRetry={() => fetchInterfaceData(deviceId, interfaceName)}
/>
}
>
{(data) => <InterfaceData interfaceData={data} />}
{(data) =>
filteredData ? (
<ObjectDatastreamTable key={filteredData.endpoint} dataTreeNode={filteredData} />
) : (
<InterfaceData interfaceData={data} />
)
}
</WaitForData>
</Card.Body>
</Card>
Expand All @@ -603,6 +747,14 @@ export default (): React.ReactElement => {
sendInterfaceData={sendInterfaceData}
/>
)}
{showTimeModal && (
<TimeRangeModal
showTimeModal={showTimeModal}
requestingData={requestingData}
handleShowTimeModal={handleShowTimeModal}
fetchInterfaceTimeData={fetchInterfaceData}
/>
)}
</Container>
);
};

0 comments on commit dddf217

Please sign in to comment.