Initial commit

This commit is contained in:
Anthony Hinsinger
2020-06-15 09:15:22 +02:00
parent b1973cd2ba
commit 1177ac1c61
19 changed files with 1206 additions and 37 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
.App {
text-align: center;
padding: 20px;
}
.App-logo {
+15 -19
View File
@@ -1,25 +1,21 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import React from "react";
import logo from "./logo.svg";
//import "./App.css";
import { Grommet, Box } from "grommet";
import client from "./apolloclient";
import { ApolloProvider } from "@apollo/react-hooks";
import Devices from "./devices";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<ApolloProvider client={client}>
<Grommet full>
<Box fill>
<Devices />
</Box>
</Grommet>
</ApolloProvider>
);
}
+242
View File
@@ -0,0 +1,242 @@
import { ApolloClient } from "apollo-client";
import {
InMemoryCache,
IntrospectionFragmentMatcher,
} from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import { onError } from "apollo-link-error";
import { ApolloLink, split } from "apollo-link";
import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import gql from "graphql-tag";
import introspectionQueryResultData from "./fragmentTypes.json";
import {
propertyVectorFragment,
vectorFragment,
deviceInfoFragment,
} from "./graphql/fragment";
import { isConnectedQuery } from "./graphql/query";
// Create an http link:
const httpLink = new HttpLink({
uri: "/graphql",
});
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: `ws://${window.location.host}/graphql`,
options: {
reconnect: true,
},
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
link,
]),
cache: new InMemoryCache({
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),
cacheRedirects: {
Query: {
device: (_, args, { getCacheKey }) =>
getCacheKey({ __typename: "Device", id: args.id }),
},
},
}),
});
const CONNECTED = gql`
subscription connected {
connected
}
`;
client
.subscribe({
query: CONNECTED,
})
.subscribe(({ data }) => {
client.writeQuery({
query: isConnectedQuery,
data: { connected: data.connected },
});
console.log(data);
});
const DISCONNECTED = gql`
subscription disconnected {
disconnected
}
`;
client
.subscribe({
query: DISCONNECTED,
})
.subscribe(({ data }) => {
console.log(data);
});
const NEW_DEVICE = gql`
subscription newDevice {
newDevice {
id
...DeviceInfo
}
}
${deviceInfoFragment}
`;
const GETDEVICES = gql`
query devices {
devices {
id
...DeviceInfo
properties {
id
name
...PropertyVector
}
}
}
${deviceInfoFragment}
${propertyVectorFragment}
`;
const GETDEVICE = gql`
query device($id: String!) {
device(id: $id) {
id
...DeviceInfo
properties {
id
name
...PropertyVector
}
}
}
${deviceInfoFragment}
${propertyVectorFragment}
`;
client
.subscribe({
query: NEW_DEVICE,
})
.subscribe(({ data }) => {
const res = client.readQuery({ query: GETDEVICES });
data.newDevice.properties = [];
client.writeQuery({
query: GETDEVICES,
data: { devices: [...res.devices, data.newDevice] },
});
});
const NEW_PROPERTY = gql`
subscription newProperty {
newProperty {
id
name
device
...PropertyVector
}
}
${propertyVectorFragment}
`;
client
.subscribe({
query: NEW_PROPERTY,
})
.subscribe(({ data }) => {
const res = client.readQuery({
query: GETDEVICE,
variables: { id: data.newProperty.device },
});
client.writeQuery({
query: GETDEVICE,
variables: { id: res.device.id },
data: {
device: {
...res.device,
drivers:
data.newProperty.name === "DRIVER_INFO"
? data.newProperty.vector.values[3].text
: res.device.drivers,
connected:
data.newProperty.name === "CONNECTION"
? data.newProperty.vector.values[0].switch
: res.device.connected,
properties: [...res.device.properties, data.newProperty],
},
},
});
});
const NEW_VALUE = gql`
subscription newValue {
newValue {
id
device
...VectorData
}
}
${vectorFragment}
`;
client
.subscribe({
query: NEW_VALUE,
})
.subscribe(({ data }) => {
if (data.newValue.name === "CONNECTION") {
const res = client.readQuery({
query: GETDEVICE,
variables: { id: data.newValue.device },
});
client.writeQuery({
query: GETDEVICE,
variables: { id: res.device.id },
data: {
device: {
...res.device,
connected: data.newValue.values[0].switch,
},
},
});
console.log(res);
}
});
export default client;
+64
View File
@@ -0,0 +1,64 @@
import React, { useState, useEffect } from "react";
import { useMutation } from "@apollo/react-hooks";
import { driversToInterfaces } from "../../utils/indi";
import Property from "./property";
import gql from "graphql-tag";
import { Box, Button, Tabs, Tab } from "grommet";
const CONNECT_DEVICE = gql`
mutation connectDevice($id: String!) {
connectDevice(id: $id) {
id
}
}
`;
const Device = ({ device }) => {
const [connect] = useMutation(CONNECT_DEVICE, {
variables: { id: device.id },
});
const [groups, setGroups] = useState([]);
const [selectedTabs, setSelectedTabs] = useState({});
const [currentTab, setCurrentTab] = useState(0);
useEffect(() => {
const groups = new Set();
device.properties.forEach((v) => {
groups.add(v.vector.group);
});
setGroups(Array.from(groups));
setCurrentTab(selectedTabs[device.id] || 0);
}, [device]);
return (
<Box>
{!device.connected && (
<Button
label="Connect"
onClick={() => {
connect();
}}
/>
)}
<Tabs justify="start" activeIndex={currentTab} onActive={(nextTab) => {
setCurrentTab(nextTab);
const newValue = { ...selectedTabs };
newValue[device.id] = nextTab;
setSelectedTabs(newValue)
}}>
{groups.map((g) => (
<Tab title={g} key={g}>
<Box pad="small">
{device.properties &&
device.properties
.filter((p) => p.vector.group === g)
.map((p) => <Property property={p} key={p.id} />)}
</Box>
</Tab>
))}
</Tabs>
</Box>
);
};
export default Device;
+15
View File
@@ -0,0 +1,15 @@
import React from "react";
import { stateToEmoji } from "../../utils/indi";
const LightVector = ({ vector }) => {
return (
<div>
{vector.values.map(v => (
<div key={v.name}>{stateToEmoji(v.switch)} {v.label || v.name}</div>
))}
</div>
);
};
export default LightVector;
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import { Box, TextInput, Button } from "grommet";
const NumberVector = ({ vector }) => {
return (
<Box gap="xsmall">
{vector.values.map((v) => (
<Box key={v.name} direction="row" align="center">
<Box width="small">{v.label || v.name} ({v.min},{v.max},{v.step})</Box>
<Box width="small">{v.formated || v.number}</Box>
{vector.permission !== 0 && (
<Box direction="row" align="center" gap="xsmall">
<TextInput></TextInput>
<Button primary label="Set"></Button>
</Box>
)}
</Box>
))}
</Box>
);
};
export default NumberVector;
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
import { Box } from "grommet";
import { stateToEmoji } from "../../utils/indi";
import LightVector from "./light";
import NumberVector from "./number";
import SwitchVector from "./switch";
import TextVector from "./text";
const Property = ({ property }) => {
return (
<Box direction="row" margin={{bottom: "medium"}} align="center">
<Box width="medium">
<div>
{stateToEmoji(property.vector.state)}{" "}
<strong>{property.vector.label || property.name}</strong>
</div>
</Box>
{property.vector.__typename === "NumberVector" && (
<NumberVector vector={property.vector} />
)}
{property.vector.__typename === "SwitchVector" && (
<SwitchVector vector={property.vector} />
)}
{property.vector.__typename === "TextVector" && (
<TextVector vector={property.vector} />
)}
{property.vector.__typename === "LightVector" && (
<LightVector vector={property.vector} />
)}
</Box>
);
};
export default Property;
+53
View File
@@ -0,0 +1,53 @@
import React from "react";
import { Button, Box } from "grommet";
import gql from "graphql-tag";
import { useMutation } from "@apollo/react-hooks";
const SEND = gql`
mutation sendSwitch($input: SwitchInput) {
sendSwitch(input: $input)
}
`;
const SwitchVector = ({ vector }) => {
const [sendSwitch] = useMutation(SEND);
return (
<Box direction="row" gap="xsmall">
{vector.values.map((v) => (
<Button
primary={v.switch}
key={v.name}
label={v.label}
onClick={() => {
const newValues = vector.values.map((val) => {
if (v.name === val.name) {
return { name: val.name, value: true };
} else {
return {
name: val.name,
value: vector.rule === 0 ? false : val.switch,
};
}
});
const input = {
device: vector.device,
property: vector.name,
values: newValues,
};
console.log(vector.rule);
console.log(input);
sendSwitch({
variables: { input },
}).then((data) => {
console.log(data);
});
}}
></Button>
))}
</Box>
);
};
export default SwitchVector;
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import { Box, Button, TextInput } from "grommet";
const TextVector = ({ vector }) => {
return (
<Box gap="xsmall">
{vector.values.map((v) => (
<Box key={v.name} direction="row" align="center">
<Box width="small">{v.label || v.name}</Box>
<Box width="small">{v.text}</Box>
{vector.permission !== 0 && (
<Box direction="row" align="center" gap="xsmall">
<TextInput value={v.text}></TextInput>
<Button primary label="Set"></Button>
</Box>
)}
</Box>
))}
</Box>
);
};
export default TextVector;
+104
View File
@@ -0,0 +1,104 @@
import React, { useEffect, useState } from "react";
import { useQuery, useSubscription, useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";
import { deviceInfoFragment, propertyVectorFragment } from "./graphql/fragment";
import Device from "./components/indi/device";
import { Box, Tabs, Tab, Button, Header, Heading } from "grommet";
import { isConnectedQuery } from "./graphql/query";
const GETDEVICES = gql`
query devices {
devices {
id
...DeviceInfo
properties {
id
name
...PropertyVector
}
}
}
${deviceInfoFragment}
${propertyVectorFragment}
`;
const CONNECT = gql`
mutation connect {
connect
}
`;
const DISCONNECT = gql`
mutation disconnect {
disconnect
}
`;
const useIndiState = () => {
const { data } = useQuery(isConnectedQuery);
const [connected, setConnected] = useState(false);
useEffect(() => {
data && setConnected(data.connected);
}, [data]);
return connected;
};
const Devices = () => {
const connected = useIndiState();
const { data } = useQuery(GETDEVICES);
const [connect] = useMutation(CONNECT);
const [disconnect] = useMutation(DISCONNECT);
if (!data || !data.devices) {
return <div>No data</div>;
}
return (
<Box fill>
<Header background="brand" pad="small">
<Box>
<Heading margin="xsmall">ASTRAW</Heading>
</Box>
<Box direction="row" gap="medium">
{!connected ? (
<Button
label="Connect"
onClick={() => {
connect();
}}
/>
) : (
<Button
label="Disconnect"
onClick={() => {
disconnect();
}}
/>
)}
</Box>
</Header>
<Box overflow="auto">
{data ? (
<Tabs justify="start" key="d.id">
{data && data.devices ? (
data.devices.map((d) => (
<Tab title={d.name} key={d.id}>
<Device device={d} />
</Tab>
))
) : (
<div>no devices</div>
)}
</Tabs>
) : null}
</Box>
</Box>
);
};
export default Devices;
+1
View File
@@ -0,0 +1 @@
{"__schema":{"types":[{"kind":"INTERFACE","name":"Vector","possibleTypes":[{"name":"NumberVector"},{"name":"SwitchVector"},{"name":"TextVector"},{"name":"LightVector"}]}]}}
+66
View File
@@ -0,0 +1,66 @@
import gql from "graphql-tag";
const deviceInfoFragment = gql`
fragment DeviceInfo on Device {
name
connected
drivers
}
`;
const vectorFragment = gql`
fragment VectorData on Vector {
name
label
group
device
state
permission
... on NumberVector {
values {
name
label
number:value
min
max
step
formated
}
}
... on SwitchVector {
rule
values {
name
label
switch:value
}
}
... on TextVector {
values {
name
label
text:value
}
}
... on LightVector {
values {
name
label
light:value
}
}
}
`;
const propertyVectorFragment = gql`
fragment PropertyVector on Property {
vector {
id
...VectorData
}
}
${vectorFragment}
`;
export { deviceInfoFragment, vectorFragment, propertyVectorFragment };
+9
View File
@@ -0,0 +1,9 @@
import gql from "graphql-tag";
const isConnectedQuery = gql`
query connected {
connected
}
`;
export { isConnectedQuery }
+6 -6
View File
@@ -1,14 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
+5
View File
@@ -0,0 +1,5 @@
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(proxy('/graphql', { target: 'http://localhost:4000', ws: true }));
};
+44
View File
@@ -0,0 +1,44 @@
const INTERFACES = {
GENERAL: 0,
TELESCOPE: 1 << 0,
CCD: 1 << 1,
GUIDER: 1 << 2,
FOCUSER: 1 << 3,
};
/*
FILTER
DOME
GPS
WEATHER
AO
DUSTCAP
LIGHTBOX
DETECTOR
ROTATOR
SPECTROGRAPH
CORRELATOR
AUX
}
*/
export function driversToInterfaces(drivers) {
const ret = [];
for (const iface in INTERFACES) {
if (INTERFACES[iface] & drivers) {
ret.push(iface);
}
}
return ret;
}
export function stateToEmoji(state) {
switch (state) {
case 0:
return "⚪";
case 1:
return "🟢";
case 2:
return "🟡";
case 3:
return "🔴";
}
}